File indexing completed on 2025-01-05 04:46:55
0001 /* 0002 * SPDX-FileCopyrightText: 2014 Daniel Vrátil <dvratil@redhat.com> 0003 * SPDX-FileCopyrightText: 2016 Daniel Vrátil <dvratil@kde.org> 0004 * 0005 * SPDX-License-Identifier: LGPL-2.1-or-later 0006 * 0007 */ 0008 0009 #include "collectionstatistics.h" 0010 #include "akonadiserver_debug.h" 0011 #include "countquerybuilder.h" 0012 #include "datastore.h" 0013 #include "entities.h" 0014 #include "querybuilder.h" 0015 0016 #include "private/protocol_p.h" 0017 0018 using namespace Akonadi::Server; 0019 0020 CollectionStatistics::CollectionStatistics(bool prefetch) 0021 { 0022 if (prefetch) { 0023 QMutexLocker lock(&mCacheLock); 0024 0025 QList<QueryBuilder> builders; 0026 // This single query will give us statistics for all non-empty non-virtual 0027 // Collections at much better speed than individual queries. 0028 auto qb = prepareGenericQuery(); 0029 qb.addColumn(PimItem::collectionIdFullColumnName()); 0030 qb.addGroupColumn(PimItem::collectionIdFullColumnName()); 0031 builders << qb; 0032 0033 // This single query will give us statistics for all non-empty virtual 0034 // Collections 0035 qb = prepareGenericQuery(); 0036 qb.addColumn(CollectionPimItemRelation::leftFullColumnName()); 0037 qb.addJoin(QueryBuilder::InnerJoin, 0038 CollectionPimItemRelation::tableName(), 0039 CollectionPimItemRelation::rightFullColumnName(), 0040 PimItem::idFullColumnName()); 0041 qb.addGroupColumn(CollectionPimItemRelation::leftFullColumnName()); 0042 builders << qb; 0043 0044 for (auto &qb : builders) { 0045 if (!qb.exec()) { 0046 return; 0047 } 0048 0049 auto query = qb.query(); 0050 while (query.next()) { 0051 mCache.insert(query.value(3).toLongLong(), {query.value(0).toLongLong(), query.value(1).toLongLong(), query.value(2).toLongLong()}); 0052 } 0053 } 0054 0055 // Now quickly get all non-virtual enabled Collections and if they are 0056 // not in mCache yet, insert them with empty statistics. 0057 qb = QueryBuilder(Collection::tableName()); 0058 qb.addColumn(Collection::idColumn()); 0059 qb.addValueCondition(Collection::enabledColumn(), Query::Equals, true); 0060 qb.addValueCondition(Collection::isVirtualColumn(), Query::Equals, false); 0061 if (!qb.exec()) { 0062 return; 0063 } 0064 0065 auto query = qb.query(); 0066 while (query.next()) { 0067 const auto colId = query.value(0).toLongLong(); 0068 if (!mCache.contains(colId)) { 0069 mCache.insert(colId, {0, 0, 0}); 0070 } 0071 } 0072 } 0073 } 0074 0075 void CollectionStatistics::itemAdded(const Collection &col, qint64 size, bool seen) 0076 { 0077 if (!col.isValid()) { 0078 return; 0079 } 0080 0081 QMutexLocker lock(&mCacheLock); 0082 auto stats = mCache.find(col.id()); 0083 if (stats != mCache.end()) { 0084 ++(stats->count); 0085 stats->size += size; 0086 stats->read += (seen ? 1 : 0); 0087 } else { 0088 mCache.insert(col.id(), calculateCollectionStatistics(col)); 0089 } 0090 } 0091 0092 void CollectionStatistics::itemsSeenChanged(const Collection &col, qint64 seenCount) 0093 { 0094 if (!col.isValid()) { 0095 return; 0096 } 0097 0098 QMutexLocker lock(&mCacheLock); 0099 auto stats = mCache.find(col.id()); 0100 if (stats != mCache.end()) { 0101 stats->read += seenCount; 0102 } else { 0103 mCache.insert(col.id(), calculateCollectionStatistics(col)); 0104 } 0105 } 0106 0107 void CollectionStatistics::invalidateCollection(const Collection &col) 0108 { 0109 if (!col.isValid()) { 0110 return; 0111 } 0112 0113 QMutexLocker lock(&mCacheLock); 0114 mCache.remove(col.id()); 0115 } 0116 0117 void CollectionStatistics::expireCache() 0118 { 0119 QMutexLocker lock(&mCacheLock); 0120 mCache.clear(); 0121 } 0122 0123 CollectionStatistics::Statistics CollectionStatistics::statistics(const Collection &col) 0124 { 0125 QMutexLocker lock(&mCacheLock); 0126 auto it = mCache.find(col.id()); 0127 if (it == mCache.end()) { 0128 it = mCache.insert(col.id(), calculateCollectionStatistics(col)); 0129 } 0130 return it.value(); 0131 } 0132 0133 QueryBuilder CollectionStatistics::prepareGenericQuery() 0134 { 0135 static const QString SeenFlagsTableName = QStringLiteral("SeenFlags"); 0136 static const QString IgnoredFlagsTableName = QStringLiteral("IgnoredFlags"); 0137 0138 #define FLAGS_COLUMN(table, column) QStringLiteral("%1.%2").arg(table##TableName, PimItemFlagRelation::column()) 0139 0140 // COUNT(DISTINCT PimItemTable.id) 0141 CountQueryBuilder qb(PimItem::tableName(), PimItem::idFullColumnName(), CountQueryBuilder::Distinct); 0142 // SUM(PimItemTable.size) 0143 qb.addAggregation(PimItem::sizeFullColumnName(), QStringLiteral("sum")); 0144 0145 // SUM(CASE WHEN SeenFlags.flag_id IS NOT NULL OR IgnoredFlags.flag_id IS NOT NULL THEN 1 ELSE 0 END) 0146 // This allows us to get read messages count in a single query with the other 0147 // statistics. It is much than doing two queries, because the database 0148 // only has to calculate the JOINs once. 0149 // 0150 // Flag::retrieveByName() will hit the Entity cache, which allows us to avoid 0151 // a second JOIN with FlagTable, which PostgreSQL seems to struggle to optimize. 0152 Query::Condition cond(Query::Or); 0153 cond.addValueCondition(FLAGS_COLUMN(SeenFlags, rightColumn), Query::IsNot, QVariant()); 0154 cond.addValueCondition(FLAGS_COLUMN(IgnoredFlags, rightColumn), Query::IsNot, QVariant()); 0155 0156 Query::Case caseStmt(cond, QStringLiteral("1"), QStringLiteral("0")); 0157 qb.addAggregation(caseStmt, QStringLiteral("sum")); 0158 0159 // We need to join PimItemFlagRelation table twice - once for \SEEN flag and once 0160 // for $IGNORED flag, otherwise entries from PimItemTable get duplicated when an 0161 // item has both flags and the SUM(CASE ...) above returns bogus values 0162 { 0163 Query::Condition seenCondition(Query::And); 0164 seenCondition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, FLAGS_COLUMN(SeenFlags, leftColumn)); 0165 seenCondition.addValueCondition(FLAGS_COLUMN(SeenFlags, rightColumn), 0166 Query::Equals, 0167 Flag::retrieveByNameOrCreate(QStringLiteral(AKONADI_FLAG_SEEN)).id()); 0168 qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("%1 AS %2").arg(PimItemFlagRelation::tableName(), SeenFlagsTableName), seenCondition); 0169 } 0170 { 0171 Query::Condition ignoredCondition(Query::And); 0172 ignoredCondition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, FLAGS_COLUMN(IgnoredFlags, leftColumn)); 0173 ignoredCondition.addValueCondition(FLAGS_COLUMN(IgnoredFlags, rightColumn), 0174 Query::Equals, 0175 Flag::retrieveByNameOrCreate(QStringLiteral(AKONADI_FLAG_IGNORED)).id()); 0176 qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("%1 AS %2").arg(PimItemFlagRelation::tableName(), IgnoredFlagsTableName), ignoredCondition); 0177 } 0178 0179 #undef FLAGS_COLUMN 0180 0181 return qb; 0182 } 0183 0184 CollectionStatistics::Statistics CollectionStatistics::calculateCollectionStatistics(const Collection &col) 0185 { 0186 auto qb = prepareGenericQuery(); 0187 0188 if (col.isVirtual()) { 0189 qb.addJoin(QueryBuilder::InnerJoin, 0190 CollectionPimItemRelation::tableName(), 0191 CollectionPimItemRelation::rightFullColumnName(), 0192 PimItem::idFullColumnName()); 0193 qb.addValueCondition(CollectionPimItemRelation::leftFullColumnName(), Query::Equals, col.id()); 0194 } else { 0195 qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, col.id()); 0196 } 0197 0198 if (!qb.exec()) { 0199 return {-1, -1, -1}; 0200 } 0201 if (!qb.query().next()) { 0202 qCCritical(AKONADISERVER_LOG) << "Error during retrieving result of statistics query:" << qb.query().lastError().text(); 0203 return {-1, -1, -1}; 0204 } 0205 0206 auto result = Statistics{qb.query().value(0).toLongLong(), qb.query().value(1).toLongLong(), qb.query().value(2).toLongLong()}; 0207 qb.query().finish(); 0208 return result; 0209 }