diff --git a/server/src/handler/fetchhelper.cpp b/server/src/handler/fetchhelper.cpp index a653711d..375239f5 100644 --- a/server/src/handler/fetchhelper.cpp +++ b/server/src/handler/fetchhelper.cpp @@ -1,700 +1,696 @@ /*************************************************************************** * Copyright (C) 2006-2009 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 "fetchhelper.h" #include "akdebug.h" #include "akdbus.h" #include "akonadi.h" #include "connection.h" #include "handler.h" #include "handlerhelper.h" #include "imapstreamparser.h" #include "libs/imapparser_p.h" #include "libs/protocol_p.h" #include "response.h" #include "storage/selectquerybuilder.h" #include "storage/itemqueryhelper.h" #include "storage/itemretrievalmanager.h" #include "storage/itemretrievalrequest.h" #include "storage/parthelper.h" #include #include "storage/transaction.h" #include "utils.h" #include "intervalcheck.h" #include "agentmanagerinterface.h" #include "dbusconnectionpool.h" #include "tagfetchhelper.h" #include "relationfetch.h" #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace Akonadi::Server; FetchHelper::FetchHelper( Connection *connection, const Scope &scope, const FetchScope &fetchScope ) : mStreamParser( 0 ) , mConnection( connection ) , mScope( scope ) , mFetchScope( fetchScope ) { std::fill( mItemQueryColumnMap, mItemQueryColumnMap + ItemQueryColumnCount, -1 ); } enum PartQueryColumns { PartQueryPimIdColumn, PartQueryTypeNamespaceColumn, PartQueryTypeNameColumn, PartQueryDataColumn, PartQueryExternalColumn, PartQueryVersionColumn }; QSqlQuery FetchHelper::buildPartQuery( const QVector &partList, bool allPayload, bool allAttrs ) { ///TODO: merge with ItemQuery QueryBuilder partQuery( PimItem::tableName() ); - partQuery.setForwardOnly(true); - if ( !partList.isEmpty() || allPayload || allAttrs ) { partQuery.addJoin( QueryBuilder::InnerJoin, Part::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName() ); partQuery.addJoin( QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName() ); partQuery.addColumn( PimItem::idFullColumnName() ); partQuery.addColumn( PartType::nsFullColumnName() ); partQuery.addColumn( PartType::nameFullColumnName() ); partQuery.addColumn( Part::dataFullColumnName() ); partQuery.addColumn( Part::externalFullColumnName() ); partQuery.addColumn( Part::versionFullColumnName() ); partQuery.addSortColumn( PimItem::idFullColumnName(), Query::Descending ); Query::Condition cond( Query::Or ); if ( !partList.isEmpty() ) { QStringList partNameList; Q_FOREACH ( const QByteArray &b, partList ) { if ( b.startsWith( "PLD" ) || b.startsWith( "ATR" ) ) { partNameList.push_back( QString::fromLatin1( b ) ); } } if ( !partNameList.isEmpty() ) { cond.addCondition( PartTypeHelper::conditionFromFqNames( partNameList ) ); } } if ( allPayload ) { cond.addValueCondition( PartType::nsFullColumnName(), Query::Equals, QLatin1String( "PLD" ) ); } if ( allAttrs ) { cond.addValueCondition( PartType::nsFullColumnName(), Query::Equals, QLatin1String( "ATR" ) ); } if ( !cond.isEmpty() ) { partQuery.addCondition( cond ); } ItemQueryHelper::scopeToQuery( mScope, mConnection->context(), partQuery ); if ( !partQuery.exec() ) { throw HandlerException( "Unable to list item parts" ); } partQuery.query().next(); } return partQuery.query(); } QSqlQuery FetchHelper::buildItemQuery() { QueryBuilder itemQuery( PimItem::tableName() ); - itemQuery.setForwardOnly(true); - itemQuery.addJoin( QueryBuilder::InnerJoin, MimeType::tableName(), PimItem::mimeTypeIdFullColumnName(), MimeType::idFullColumnName() ); int column = 0; #define ADD_COLUMN(colName, colId) { itemQuery.addColumn( colName ); mItemQueryColumnMap[colId] = column++; } ADD_COLUMN( PimItem::idFullColumnName(), ItemQueryPimItemIdColumn ); if ( mFetchScope.remoteIdRequested() ) { ADD_COLUMN( PimItem::remoteIdFullColumnName(), ItemQueryPimItemRidColumn ) } ADD_COLUMN( MimeType::nameFullColumnName(), ItemQueryMimeTypeColumn ) ADD_COLUMN( PimItem::revFullColumnName(), ItemQueryRevColumn ) if ( mFetchScope.remoteRevisionRequested() ) { ADD_COLUMN( PimItem::remoteRevisionFullColumnName(), ItemQueryRemoteRevisionColumn ) } if ( mFetchScope.sizeRequested() ) { ADD_COLUMN( PimItem::sizeFullColumnName(), ItemQuerySizeColumn ) } if ( mFetchScope.mTimeRequested() ) { ADD_COLUMN( PimItem::datetimeFullColumnName(), ItemQueryDatetimeColumn ) } ADD_COLUMN( PimItem::collectionIdFullColumnName(), ItemQueryCollectionIdColumn ) if ( mFetchScope.gidRequested() ) { ADD_COLUMN( PimItem::gidFullColumnName(), ItemQueryPimItemGidColumn ) } #undef ADD_COLUMN itemQuery.addSortColumn( PimItem::idFullColumnName(), Query::Descending ); if ( mScope.scope() != Scope::Invalid ) { ItemQueryHelper::scopeToQuery( mScope, mConnection->context(), itemQuery ); } if ( mFetchScope.changedSince().isValid() ) { itemQuery.addValueCondition( PimItem::datetimeFullColumnName(), Query::GreaterOrEqual, mFetchScope.changedSince().toUTC() ); } if ( !itemQuery.exec() ) { throw HandlerException( "Unable to list items" ); } itemQuery.query().next(); return itemQuery.query(); } enum FlagQueryColumns { FlagQueryIdColumn, FlagQueryNameColumn }; QSqlQuery FetchHelper::buildFlagQuery() { QueryBuilder flagQuery( PimItem::tableName() ); flagQuery.addJoin( QueryBuilder::InnerJoin, PimItemFlagRelation::tableName(), PimItem::idFullColumnName(), PimItemFlagRelation::leftFullColumnName() ); flagQuery.addJoin( QueryBuilder::InnerJoin, Flag::tableName(), Flag::idFullColumnName(), PimItemFlagRelation::rightFullColumnName() ); flagQuery.addColumn( PimItem::idFullColumnName() ); flagQuery.addColumn( Flag::nameFullColumnName() ); ItemQueryHelper::scopeToQuery( mScope, mConnection->context(), flagQuery ); flagQuery.addSortColumn( PimItem::idFullColumnName(), Query::Descending ); if ( !flagQuery.exec() ) { throw HandlerException( "Unable to retrieve item flags" ); } flagQuery.query().next(); return flagQuery.query(); } enum TagQueryColumns { TagQueryItemIdColumn, TagQueryTagIdColumn, }; QSqlQuery FetchHelper::buildTagQuery() { QueryBuilder tagQuery( PimItem::tableName() ); tagQuery.addJoin( QueryBuilder::InnerJoin, PimItemTagRelation::tableName(), PimItem::idFullColumnName(), PimItemTagRelation::leftFullColumnName() ); tagQuery.addJoin( QueryBuilder::InnerJoin, Tag::tableName(), Tag::idFullColumnName(), PimItemTagRelation::rightFullColumnName() ); tagQuery.addColumn( PimItem::idFullColumnName() ); tagQuery.addColumn( Tag::idFullColumnName() ); ItemQueryHelper::scopeToQuery( mScope, mConnection->context(), tagQuery ); tagQuery.addSortColumn( PimItem::idFullColumnName(), Query::Descending ); if ( !tagQuery.exec() ) { throw HandlerException( "Unable to retrieve item tags" ); } tagQuery.query().next(); return tagQuery.query(); } enum VRefQueryColumns { VRefQueryCollectionIdColumn, VRefQueryItemIdColumn }; QSqlQuery FetchHelper::buildVRefQuery() { QueryBuilder vRefQuery( PimItem::tableName() ); vRefQuery.addJoin( QueryBuilder::LeftJoin, CollectionPimItemRelation::tableName(), CollectionPimItemRelation::rightFullColumnName(), PimItem::idFullColumnName() ); vRefQuery.addColumn( CollectionPimItemRelation::leftFullColumnName() ); vRefQuery.addColumn( CollectionPimItemRelation::rightFullColumnName() ); ItemQueryHelper::scopeToQuery( mScope, mConnection->context(), vRefQuery ); vRefQuery.addSortColumn( PimItem::idFullColumnName(), Query::Descending ); if (!vRefQuery.exec() ) { throw HandlerException( "Unable to retrieve virtual references" ); } vRefQuery.query().next(); return vRefQuery.query(); } bool FetchHelper::isScopeLocal( const Scope &scope ) { // The only agent allowed to override local scope is the Baloo Indexer if ( !mConnection->sessionId().startsWith( "akonadi_baloo_indexer" ) ) { return false; } // Get list of all resources that own all items in the scope QueryBuilder qb( PimItem::tableName(), QueryBuilder::Select ); qb.setDistinct( true ); qb.addColumn( Resource::nameFullColumnName() ); qb.addJoin( QueryBuilder::LeftJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName() ); qb.addJoin( QueryBuilder::LeftJoin, Resource::tableName(), Collection::resourceIdFullColumnName(), Resource::idFullColumnName() ); ItemQueryHelper::scopeToQuery( scope, mConnection->context(), qb ); if ( mConnection->context()->resource().isValid() ) { qb.addValueCondition( Resource::nameFullColumnName(), Query::NotEquals, mConnection->context()->resource().name() ); } if ( !qb.exec() ) { throw HandlerException( "Failed to query database" ); return false; } // If there is more than one resource, i.e. this is a fetch from multiple // collections, then don't bother and just return FALSE. This case is aimed // specifically on Baloo, which fetches items from each collection independently, // so it will pass this check. QSqlQuery query = qb.query(); if ( query.size() != 1) { return false; } query.next(); const QString resourceName = query.value( 0 ).toString(); org::freedesktop::Akonadi::AgentManager manager( AkDBus::serviceName( AkDBus::Control ), QLatin1String( "/AgentManager" ), DBusConnectionPool::threadConnection() ); const QString typeIdentifier = manager.agentInstanceType( resourceName ); const QVariantMap properties = manager.agentCustomProperties( typeIdentifier ); return properties.value( QLatin1String( "HasLocalStorage" ), false ).toBool(); } QByteArray FetchHelper::tagsToByteArray( const Tag::List &tags ) { QByteArray b; b += "("; Q_FOREACH ( const Tag &tag, tags ) { b += "(" + TagFetchHelper::tagToByteArray( tag.id(), tag.gid().toLatin1(), tag.parentId(), tag.tagType().name().toLatin1(), QByteArray(), TagFetchHelper::fetchTagAttributes( tag.id() ) ) + ") "; } b += ")"; return b; } QByteArray FetchHelper::relationsToByteArray(const Relation::List &relations) { QByteArray b; b += "("; Q_FOREACH (const Relation &relation, relations) { b += "(" + RelationFetch::relationToByteArray(relation.leftId(), relation.rightId(), relation.relationType().name().toLatin1(), relation.remoteId().toLatin1()) + ") "; } b += ")"; return b; } bool FetchHelper::fetchItems( const QByteArray &responseIdentifier ) { // retrieve missing parts // HACK: isScopeLocal() is a workaround for resources that have cache expiration // because when the cache expires, Baloo is not able to content of the items. So // we allow fetch of items that belong to local resources (like maildir) to ignore // cacheOnly and retrieve missing parts from the resource. However ItemRetriever // is painfully slow with many items and is generally designed to fetch a few // messages, not all of them. In the long term, we need a better way to do this. if ( !mFetchScope.cacheOnly() || isScopeLocal( mScope ) ) { // trigger a collection sync if configured to do so triggerOnDemandFetch(); // Prepare for a call to ItemRetriever::exec(); // From a resource perspective the only parts that can be fetched are payloads. ItemRetriever retriever( mConnection ); retriever.setScope( mScope ); retriever.setRetrieveParts( mFetchScope.requestedPayloads() ); retriever.setRetrieveFullPayload( mFetchScope.fullPayload() ); retriever.setChangedSince( mFetchScope.changedSince() ); if ( !retriever.exec() && !mFetchScope.ignoreErrors() ) { // There we go, retrieve the missing parts from the resource. if ( mConnection->context()->resource().isValid() ) { throw HandlerException( QString::fromLatin1( "Unable to fetch item from backend (collection %1, resource %2) : %3" ) .arg( mConnection->context()->collectionId() ) .arg( mConnection->context()->resource().id() ) .arg( QString::fromLatin1( retriever.lastError() ) ) ); } else { throw HandlerException( QString::fromLatin1( "Unable to fetch item from backend (collection %1) : %2" ) .arg( mConnection->context()->collectionId() ) .arg( QString::fromLatin1( retriever.lastError() ) ) ); } } } QSqlQuery itemQuery = buildItemQuery(); // error if query did not find any item and scope is not listing items but // a request for a specific item if ( !itemQuery.isValid() ) { if ( mFetchScope.ignoreErrors() ) { return true; } switch ( mScope.scope() ) { case Scope::Uid: // fall through case Scope::Rid: // fall through case Scope::HierarchicalRid: // fall through case Scope::Gid: throw HandlerException( "Item query returned empty result set" ); break; default: break; } } // build part query if needed QSqlQuery partQuery; if ( !mFetchScope.requestedParts().isEmpty() || mFetchScope.fullPayload() || mFetchScope.allAttributes() ) { partQuery = buildPartQuery( mFetchScope.requestedParts(), mFetchScope.fullPayload(), mFetchScope.allAttributes() ); } // build flag query if needed QSqlQuery flagQuery; if ( mFetchScope.flagsRequested() ) { flagQuery = buildFlagQuery(); } // build tag query if needed QSqlQuery tagQuery; if ( mFetchScope.tagsRequested() ) { tagQuery = buildTagQuery(); } QSqlQuery vRefQuery; if ( mFetchScope.virtualReferencesRequested() ) { vRefQuery = buildVRefQuery(); } // build responses Response response; response.setUntagged(); while ( itemQuery.isValid() ) { const qint64 pimItemId = extractQueryResult( itemQuery, ItemQueryPimItemIdColumn ).toLongLong(); const int pimItemRev = extractQueryResult( itemQuery, ItemQueryRevColumn ).toInt(); QList attributes; attributes.append( AKONADI_PARAM_UID " " + QByteArray::number( pimItemId ) ); attributes.append( AKONADI_PARAM_REVISION " " + QByteArray::number( pimItemRev ) ); if ( mFetchScope.remoteIdRequested() ) { attributes.append( AKONADI_PARAM_REMOTEID " " + ImapParser::quote( Utils::variantToByteArray( extractQueryResult( itemQuery, ItemQueryPimItemRidColumn ) ) ) ); } attributes.append( AKONADI_PARAM_MIMETYPE " " + ImapParser::quote( Utils::variantToByteArray( extractQueryResult( itemQuery, ItemQueryMimeTypeColumn ) ) ) ); Collection::Id parentCollectionId = extractQueryResult( itemQuery, ItemQueryCollectionIdColumn ).toLongLong(); attributes.append( AKONADI_PARAM_COLLECTIONID " " + QByteArray::number( parentCollectionId ) ); if ( mFetchScope.sizeRequested() ) { const qint64 pimItemSize = extractQueryResult( itemQuery, ItemQuerySizeColumn ).toLongLong(); attributes.append( AKONADI_PARAM_SIZE " " + QByteArray::number( pimItemSize ) ); } if ( mFetchScope.mTimeRequested() ) { const QDateTime pimItemDatetime = extractQueryResult( itemQuery, ItemQueryDatetimeColumn ).toDateTime(); // Date time is always stored in UTC time zone by the server. QString datetime = QLocale::c().toString( pimItemDatetime, QLatin1String( "dd-MMM-yyyy hh:mm:ss +0000" ) ); attributes.append( AKONADI_PARAM_MTIME " " + ImapParser::quote( datetime.toUtf8() ) ); } if ( mFetchScope.remoteRevisionRequested() ) { const QByteArray rrev = Utils::variantToByteArray( extractQueryResult( itemQuery, ItemQueryRemoteRevisionColumn ) ); if ( !rrev.isEmpty() ) { attributes.append( AKONADI_PARAM_REMOTEREVISION " " + ImapParser::quote( rrev ) ); } } if ( mFetchScope.gidRequested() ) { const QByteArray gid = Utils::variantToByteArray( extractQueryResult( itemQuery, ItemQueryPimItemGidColumn ) ); if ( !gid.isEmpty() ) { attributes.append( AKONADI_PARAM_GID " " + ImapParser::quote( gid ) ); } } if ( mFetchScope.flagsRequested() ) { QList flags; while ( flagQuery.isValid() ) { const qint64 id = flagQuery.value( FlagQueryIdColumn ).toLongLong(); if ( id > pimItemId ) { flagQuery.next(); continue; } else if ( id < pimItemId ) { break; } flags << Utils::variantToByteArray( flagQuery.value( FlagQueryNameColumn ) ); flagQuery.next(); } attributes.append( AKONADI_PARAM_FLAGS " (" + ImapParser::join( flags, " " ) + ')' ); } if ( mFetchScope.tagsRequested() ) { ImapSet tags; QVector tagIds; //We don't take the fetch scope into account yet. It's either id only or the full tag. const bool fullTagsRequested = !mFetchScope.tagFetchScope().isEmpty(); while ( tagQuery.isValid() ) { const qint64 id = tagQuery.value( TagQueryItemIdColumn ).toLongLong(); if ( id > pimItemId ) { tagQuery.next(); continue; } else if ( id < pimItemId ) { break; } const qint64 tagId = tagQuery.value( TagQueryTagIdColumn ).toLongLong(); tags.add( QVector() << tagId ); tagIds << tagId; tagQuery.next(); } if ( !fullTagsRequested ) { if ( !tags.isEmpty() ) { attributes.append( AKONADI_PARAM_TAGS " " + tags.toImapSequenceSet() ); } } else { Tag::List tagList; Q_FOREACH ( qint64 t, tagIds ) { tagList << Tag::retrieveById( t ); } attributes.append( AKONADI_PARAM_TAGS " " + tagsToByteArray( tagList ) ); } } if (mFetchScope.relationsRequested()) { SelectQueryBuilder qb; Query::Condition condition; condition.setSubQueryMode(Query::Or); condition.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, pimItemId); condition.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, pimItemId); qb.addCondition(condition); qb.addGroupColumns(QStringList() << Relation::leftIdColumn() << Relation::rightIdColumn() << Relation::typeIdColumn()); if (!qb.exec()) { throw HandlerException("Unable to list item relations"); } const Relation::List relations = qb.result(); attributes.append(AKONADI_PARAM_RELATIONS " " + relationsToByteArray(relations)); } if ( mFetchScope.virtualReferencesRequested() ) { ImapSet cols; while ( vRefQuery.isValid() ) { const qint64 id = vRefQuery.value( VRefQueryItemIdColumn ).toLongLong(); if ( id > pimItemId ) { vRefQuery.next(); continue; } else if ( id < pimItemId ) { break; } const qint64 collectionId = vRefQuery.value( VRefQueryCollectionIdColumn ).toLongLong(); cols.add( QVector() << collectionId ); vRefQuery.next(); } if ( !cols.isEmpty() ) { attributes.append( AKONADI_PARAM_VIRTREF " " + cols.toImapSequenceSet() ); } } if ( mFetchScope.ancestorDepth() > 0 ) { attributes.append( HandlerHelper::ancestorsToByteArray( mFetchScope.ancestorDepth(), ancestorsForItem( parentCollectionId ) ) ); } bool skipItem = false; QList cachedParts; while ( partQuery.isValid() ) { const qint64 id = partQuery.value( PartQueryPimIdColumn ).toLongLong(); if ( id > pimItemId ) { partQuery.next(); continue; } else if ( id < pimItemId ) { break; } const QByteArray partName = Utils::variantToByteArray( partQuery.value( PartQueryTypeNamespaceColumn ) ) + ':' + Utils::variantToByteArray( partQuery.value( PartQueryTypeNameColumn ) ); QByteArray part = partName; QByteArray data = Utils::variantToByteArray( partQuery.value( PartQueryDataColumn ) ); if ( mFetchScope.checkCachedPayloadPartsOnly() ) { if ( !data.isEmpty() ) { cachedParts << part; } partQuery.next(); } else { if ( mFetchScope.ignoreErrors() && data.isEmpty() ) { //We wanted the payload, couldn't get it, and are ignoring errors. Skip the item. //This is not an error though, it's fine to have empty payload parts (to denote existing but not cached parts) //akDebug() << "item" << id << "has an empty payload part in parttable for part" << partName; skipItem = true; break; } const bool partIsExternal = partQuery.value( PartQueryExternalColumn ).toBool(); if ( !mFetchScope.externalPayloadSupported() && partIsExternal ) { //external payload not supported by the client, translate the data data = PartHelper::translateData( data, partIsExternal ); } int version = partQuery.value( PartQueryVersionColumn ).toInt(); if ( version != 0 ) { // '0' is the default, so don't send it part += '[' + QByteArray::number( version ) + ']'; } if ( mFetchScope.externalPayloadSupported() && partIsExternal ) { // external data and this is supported by the client part += " [FILE] "; } if ( data.isNull() ) { part += " NIL"; } else if ( data.isEmpty() ) { part += " \"\""; } else { if ( partIsExternal ) { if ( !mConnection->capabilities().noPayloadPath() ) { data = PartHelper::resolveAbsolutePath( data ).toLocal8Bit(); } } part += " {" + QByteArray::number( data.length() ) + "}\r\n"; part += data; } if ( mFetchScope.requestedParts().contains( partName ) || mFetchScope.fullPayload() || mFetchScope.allAttributes() ) { attributes << part; } partQuery.next(); } } if ( skipItem ) { itemQuery.next(); continue; } if ( mFetchScope.checkCachedPayloadPartsOnly() ) { attributes.append( AKONADI_PARAM_CACHEDPARTS " (" + ImapParser::join( cachedParts, " " ) + ')' ); } // IMAP protocol violation: should actually be the sequence number QByteArray attr = QByteArray::number( pimItemId ) + ' ' + responseIdentifier + " ("; attr += ImapParser::join( attributes, " " ) + ')'; response.setUntagged(); response.setString( attr ); Q_EMIT responseAvailable( response ); itemQuery.next(); } // update atime (only if the payload was actually requested, otherwise a simple resource sync prevents cache clearing) if ( needsAccessTimeUpdate( mFetchScope.requestedParts() ) || mFetchScope.fullPayload() ) { updateItemAccessTime(); } return true; } bool FetchHelper::needsAccessTimeUpdate( const QVector &parts ) { // TODO technically we should compare the part list with the cache policy of // the parent collection of the retrieved items, but that's kinda expensive // Only updating the atime if the full payload was requested is a good // approximation though. return parts.contains( AKONADI_PARAM_PLD_RFC822 ); } void FetchHelper::updateItemAccessTime() { Transaction transaction( mConnection->storageBackend() ); QueryBuilder qb( PimItem::tableName(), QueryBuilder::Update ); qb.setColumnValue( PimItem::atimeColumn(), QDateTime::currentDateTime() ); ItemQueryHelper::scopeToQuery( mScope, mConnection->context(), qb ); if ( !qb.exec() ) { qWarning() << "Unable to update item access time"; } else { transaction.commit(); } } void FetchHelper::triggerOnDemandFetch() { if ( mScope.scope() != Scope::None || mConnection->context()->collectionId() <= 0 || mFetchScope.cacheOnly() ) { return; } Collection collection = mConnection->context()->collection(); // HACK: don't trigger on-demand syncing if the resource is the one triggering it if ( mConnection->sessionId() == collection.resource().name().toLatin1() ) { return; } DataStore *store = mConnection->storageBackend(); store->activeCachePolicy( collection ); if ( !collection.cachePolicySyncOnDemand() ) { return; } if ( AkonadiServer::instance()->intervalChecker() ) { AkonadiServer::instance()->intervalChecker()->requestCollectionSync( collection ); } } QStack FetchHelper::ancestorsForItem( Collection::Id parentColId ) { if ( mFetchScope.ancestorDepth() <= 0 || parentColId == 0 ) { return QStack(); } if ( mAncestorCache.contains( parentColId ) ) { return mAncestorCache.value( parentColId ); } QStack ancestors; Collection col = Collection::retrieveById( parentColId ); for ( int i = 0; i < mFetchScope.ancestorDepth(); ++i ) { if ( !col.isValid() ) { break; } ancestors.prepend( col ); col = col.parent(); } mAncestorCache.insert( parentColId, ancestors ); return ancestors; } QVariant FetchHelper::extractQueryResult( const QSqlQuery &query, FetchHelper::ItemQueryColumns column ) const { Q_ASSERT( mItemQueryColumnMap[column] >= 0 ); return query.value( mItemQueryColumnMap[column] ); } diff --git a/server/src/storage/querybuilder.cpp b/server/src/storage/querybuilder.cpp index a6bcf9e0..3421304f 100644 --- a/server/src/storage/querybuilder.cpp +++ b/server/src/storage/querybuilder.cpp @@ -1,566 +1,562 @@ /* 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 #ifndef QUERYBUILDER_UNITTEST #include "storage/datastore.h" #include "storage/querycache.h" #include "storage/storagedebugger.h" #endif #include #include using namespace Akonadi::Server; static QString 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 QString(); } static QString 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 QString(); } static QString 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 QString(); } 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( QLatin1String( "id" ) ) , mLimit( -1 ) , mDistinct( false ) { } 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 = QLatin1String( "( " ) + qb.buildQuery() + QLatin1String( " )" ); } QString 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: statement += QLatin1String( "SELECT " ); if ( mDistinct ) { statement += QLatin1String( "DISTINCT " ); } Q_ASSERT_X( mColumns.count() > 0, "QueryBuilder::exec()", "No columns specified" ); statement += mColumns.join( QLatin1String( ", " ) ); statement += QLatin1String( " FROM " ); statement += mTable; Q_FOREACH ( const QString &joinedTable, 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 " ); statement += buildWhereCondition( join.second ); } break; case Insert: { statement += QLatin1String( "INSERT INTO " ); statement += mTable; statement += QLatin1String( " (" ); typedef QPair StringVariantPair; QStringList cols, vals; Q_FOREACH ( const StringVariantPair &p, mColumnValues ) { cols.append( p.first ); vals.append( bindValue( p.second ) ); } statement += cols.join( QLatin1String( ", " ) ); statement += QLatin1String( ") VALUES (" ); statement += vals.join( 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 ) { Q_FOREACH ( const QString &table, 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( ", " ); statement += mJoinedTables.join( QLatin1String( ", " ) ); } statement += QLatin1String( " SET " ); Q_ASSERT_X( mColumnValues.count() >= 1, "QueryBuilder::exec()", "At least one column needs to be changed" ); typedef QPair StringVariantPair; QStringList updStmts; Q_FOREACH ( const StringVariantPair &p, mColumnValues ) { QString updStmt = p.first; updStmt += QLatin1String( " = " ); updStmt += bindValue( p.second ); updStmts << updStmt; } statement += updStmts.join( QLatin1String( ", " ) ); if ( mDatabaseType == DbType::PostgreSQL && !mJoinedTables.isEmpty() ) { // PSQL have this syntax // FROM t1 JOIN t2 JOIN ... statement += QLatin1String( " FROM " ); statement += mJoinedTables.join( 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 " ); statement += buildWhereCondition( whereCondition ); } if ( !mGroupColumns.isEmpty() ) { statement += QLatin1String( " GROUP BY " ); statement += mGroupColumns.join( QLatin1String( ", " ) ); } if ( !mRootCondition[HavingCondition].isEmpty() ) { statement += QLatin1String( " HAVING " ); statement += buildWhereCondition( mRootCondition[HavingCondition] ); } if ( !mSortColumns.isEmpty() ) { Q_ASSERT_X( mType == Select, "QueryBuilder::exec()", "Order statements are only valid for SELECT queries" ); QStringList orderStmts; typedef QPair SortColumnInfo; Q_FOREACH ( const SortColumnInfo &order, mSortColumns ) { QString orderStmt; orderStmt += order.first; orderStmt += sortOrderToString( order.second ); orderStmts << orderStmt; } statement += QLatin1String( " ORDER BY " ); statement += orderStmts.join( QLatin1String( ", " ) ); } if ( mLimit > 0 ) { statement += QLatin1Literal( " LIMIT " ) + QString::number( mLimit ); } return statement; } bool QueryBuilder::retryLastTransaction( bool rollback ) { #ifndef QUERYBUILDER_UNITTEST mQuery = DataStore::self()->retryLastTransaction( rollback ); return !mQuery.lastError().isValid(); #else return true; #endif } -void QueryBuilder::setForwardOnly(bool forwardOnly) -{ - mQuery.setForwardOnly(forwardOnly); -} bool QueryBuilder::exec() { const QString statement = buildQuery(); #ifndef QUERYBUILDER_UNITTEST if ( QueryCache::contains( statement ) ) { mQuery = QueryCache::query( statement ); } else { mQuery.prepare( statement ); QueryCache::insert( statement, mQuery ); } //too heavy debug info but worths to have from time to time //akDebug() << "Executing query" << statement; bool isBatch = false; for ( int i = 0; i < mBindValues.count(); ++i ) { mQuery.bindValue( QLatin1Char( ':' ) + QString::number( i ), mBindValues[i] ); if ( !isBatch && mBindValues[i].canConvert() ) { isBatch = true; } //akDebug() << 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( 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( mQuery, 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 */ ) ) ) { akDebug() << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; akDebug() << mQuery.lastError().text(); return retryLastTransaction(); } } else if ( mDatabaseType == DbType::MySQL ) { const int error = mQuery.lastError().number(); if ( error == 1213 /* ER_LOCK_DEADLOCK */ ) { akDebug() << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; akDebug() << mQuery.lastError().text(); return retryLastTransaction(); } else if ( error == 1205 /* ER_LOCK_WAIT_TIMEOUT */ ) { akDebug() << "QueryBuilder::exec(): database reported transaction timeout, retrying transaction"; akDebug() << mQuery.lastError().text(); return retryLastTransaction(); } } else if ( mDatabaseType == DbType::Sqlite && !DbType::isSystemSQLite( DataStore::self()->database() ) ) { const int error = mQuery.lastError().number(); if ( error == 6 /* SQLITE_LOCKED */ ) { akDebug() << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; akDebug() << mQuery.lastError().text(); return retryLastTransaction( true ); } else if ( error == 5 /* SQLITE_BUSY */ ) { akDebug() << "QueryBuilder::exec(): database reported transaction timeout, retrying transaction"; akDebug() << 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. } akError() << "DATABASE ERROR:"; akError() << " Error code:" << mQuery.lastError().number(); akError() << " DB error: " << mQuery.lastError().databaseText(); akError() << " Error text:" << mQuery.lastError().text(); akError() << " 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::addAggregation( const QString &col, const QString &aggregate ) { QString s( aggregate ); s += QLatin1Char( '(' ); s += col; s += QLatin1Char( ')' ); mColumns.append( s ); } QString QueryBuilder::bindValue( const QVariant &value ) { mBindValues << value; return QLatin1Char( ':' ) + QString::number( mBindValues.count() - 1 ); } QString QueryBuilder::buildWhereCondition( const Query::Condition &cond ) { if ( !cond.isEmpty() ) { QStringList conds; Q_FOREACH ( const Query::Condition &c, cond.subConditions() ) { conds << buildWhereCondition( c ); } return QLatin1String( "( " ) + conds.join( logicOperatorToString( cond.mCombineOp ) ) + QLatin1String( " )" ); } else { QString stmt = cond.mColumn; stmt += compareOperatorToString( cond.mCompareOp ); if ( cond.mComparedColumn.isEmpty() ) { if ( cond.mComparedValue.isValid() ) { if ( cond.mComparedValue.canConvert( QVariant::List ) ) { stmt += QLatin1String( "( " ); QStringList entries; Q_ASSERT_X( !cond.mComparedValue.toList().isEmpty(), "QueryBuilder::buildWhereCondition()", "No values given for IN condition." ); Q_FOREACH ( const QVariant &entry, cond.mComparedValue.toList() ) { entries << bindValue( entry ); } stmt += entries.join( QLatin1String( ", " ) ); stmt += QLatin1String( " )" ); } else { stmt += bindValue( cond.mComparedValue ); } } else { stmt += QLatin1String( "NULL" ); } } else { stmt += cond.mComparedColumn; } return stmt; } } 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 ) { Q_ASSERT( !mIdentificationColumn.isEmpty() ); query().next(); 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; } diff --git a/server/src/storage/querybuilder.h b/server/src/storage/querybuilder.h index 701471e9..b380f93d 100644 --- a/server/src/storage/querybuilder.h +++ b/server/src/storage/querybuilder.h @@ -1,279 +1,277 @@ /* 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 }; /** 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 an aggregation statement. * @param col The column to aggregate on * @param aggregate The aggregation function. */ void addAggregation( const QString &col, 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(); - void setForwardOnly(bool forwardOnly); - private: QString buildQuery(); QString bindValue( const QVariant &value ); QString buildWhereCondition( const Query::Condition &cond ); /** * 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; QHash mRootCondition; QSqlQuery mQuery; QueryType mType; QStringList mColumns; QList 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; #ifdef QUERYBUILDER_UNITTEST QString mStatement; friend class ::QueryBuilderTest; #endif }; } // namespace Server } // namespace Akonadi #endif