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 }