Akonadi

server/storage/collectionstatistics.cpp
1/*
2 * SPDX-FileCopyrightText: 2014 Daniel Vrátil <dvratil@redhat.com>
3 * SPDX-FileCopyrightText: 2016 Daniel Vrátil <dvratil@kde.org>
4 *
5 * SPDX-License-Identifier: LGPL-2.1-or-later
6 *
7 */
8
9#include "collectionstatistics.h"
10#include "akonadiserver_debug.h"
11#include "countquerybuilder.h"
12#include "datastore.h"
13#include "entities.h"
14#include "querybuilder.h"
15
16#include "private/protocol_p.h"
17
18using namespace Akonadi::Server;
19
20CollectionStatistics::CollectionStatistics(bool prefetch)
21{
22 if (prefetch) {
23 QMutexLocker lock(&mCacheLock);
24
25 QList<QueryBuilder> builders;
26 // This single query will give us statistics for all non-empty non-virtual
27 // Collections at much better speed than individual queries.
28 auto qb = prepareGenericQuery();
29 qb.addColumn(PimItem::collectionIdFullColumnName());
30 qb.addGroupColumn(PimItem::collectionIdFullColumnName());
31 builders << qb;
32
33 // This single query will give us statistics for all non-empty virtual
34 // Collections
35 qb = prepareGenericQuery();
36 qb.addColumn(CollectionPimItemRelation::leftFullColumnName());
37 qb.addJoin(QueryBuilder::InnerJoin,
38 CollectionPimItemRelation::tableName(),
39 CollectionPimItemRelation::rightFullColumnName(),
40 PimItem::idFullColumnName());
41 qb.addGroupColumn(CollectionPimItemRelation::leftFullColumnName());
42 builders << qb;
43
44 for (auto &qb : builders) {
45 if (!qb.exec()) {
46 return;
47 }
48
49 auto query = qb.query();
50 while (query.next()) {
51 mCache.insert(query.value(3).toLongLong(), {query.value(0).toLongLong(), query.value(1).toLongLong(), query.value(2).toLongLong()});
52 }
53 }
54
55 // Now quickly get all non-virtual enabled Collections and if they are
56 // not in mCache yet, insert them with empty statistics.
57 qb = QueryBuilder(Collection::tableName());
58 qb.addColumn(Collection::idColumn());
59 qb.addValueCondition(Collection::enabledColumn(), Query::Equals, true);
60 qb.addValueCondition(Collection::isVirtualColumn(), Query::Equals, false);
61 if (!qb.exec()) {
62 return;
63 }
64
65 auto query = qb.query();
66 while (query.next()) {
67 const auto colId = query.value(0).toLongLong();
68 if (!mCache.contains(colId)) {
69 mCache.insert(colId, {0, 0, 0});
70 }
71 }
72 }
73}
74
75void CollectionStatistics::itemAdded(const Collection &col, qint64 size, bool seen)
76{
77 if (!col.isValid()) {
78 return;
79 }
80
81 QMutexLocker lock(&mCacheLock);
82 auto stats = mCache.find(col.id());
83 if (stats != mCache.end()) {
84 ++(stats->count);
85 stats->size += size;
86 stats->read += (seen ? 1 : 0);
87 } else {
88 mCache.insert(col.id(), calculateCollectionStatistics(col));
89 }
90}
91
92void CollectionStatistics::itemsSeenChanged(const Collection &col, qint64 seenCount)
93{
94 if (!col.isValid()) {
95 return;
96 }
97
98 QMutexLocker lock(&mCacheLock);
99 auto stats = mCache.find(col.id());
100 if (stats != mCache.end()) {
101 stats->read += seenCount;
102 } else {
103 mCache.insert(col.id(), calculateCollectionStatistics(col));
104 }
105}
106
107void CollectionStatistics::invalidateCollection(const Collection &col)
108{
109 if (!col.isValid()) {
110 return;
111 }
112
113 QMutexLocker lock(&mCacheLock);
114 mCache.remove(col.id());
115}
116
117void CollectionStatistics::expireCache()
118{
119 QMutexLocker lock(&mCacheLock);
120 mCache.clear();
121}
122
123CollectionStatistics::Statistics CollectionStatistics::statistics(const Collection &col)
124{
125 QMutexLocker lock(&mCacheLock);
126 auto it = mCache.find(col.id());
127 if (it == mCache.end()) {
128 it = mCache.insert(col.id(), calculateCollectionStatistics(col));
129 }
130 return it.value();
131}
132
133QueryBuilder CollectionStatistics::prepareGenericQuery()
134{
135 static const QString SeenFlagsTableName = QStringLiteral("SeenFlags");
136 static const QString IgnoredFlagsTableName = QStringLiteral("IgnoredFlags");
137
138#define FLAGS_COLUMN(table, column) QStringLiteral("%1.%2").arg(table##TableName, PimItemFlagRelation::column())
139
140 // COUNT(DISTINCT PimItemTable.id)
141 CountQueryBuilder qb(PimItem::tableName(), PimItem::idFullColumnName(), CountQueryBuilder::Distinct);
142 // SUM(PimItemTable.size)
143 qb.addAggregation(PimItem::sizeFullColumnName(), QStringLiteral("sum"));
144
145 // SUM(CASE WHEN SeenFlags.flag_id IS NOT NULL OR IgnoredFlags.flag_id IS NOT NULL THEN 1 ELSE 0 END)
146 // This allows us to get read messages count in a single query with the other
147 // statistics. It is much than doing two queries, because the database
148 // only has to calculate the JOINs once.
149 //
150 // Flag::retrieveByName() will hit the Entity cache, which allows us to avoid
151 // a second JOIN with FlagTable, which PostgreSQL seems to struggle to optimize.
152 Query::Condition cond(Query::Or);
153 cond.addValueCondition(FLAGS_COLUMN(SeenFlags, rightColumn), Query::IsNot, QVariant());
154 cond.addValueCondition(FLAGS_COLUMN(IgnoredFlags, rightColumn), Query::IsNot, QVariant());
155
156 Query::Case caseStmt(cond, QStringLiteral("1"), QStringLiteral("0"));
157 qb.addAggregation(caseStmt, QStringLiteral("sum"));
158
159 // We need to join PimItemFlagRelation table twice - once for \SEEN flag and once
160 // for $IGNORED flag, otherwise entries from PimItemTable get duplicated when an
161 // item has both flags and the SUM(CASE ...) above returns bogus values
162 {
163 Query::Condition seenCondition(Query::And);
164 seenCondition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, FLAGS_COLUMN(SeenFlags, leftColumn));
165 seenCondition.addValueCondition(FLAGS_COLUMN(SeenFlags, rightColumn),
166 Query::Equals,
167 Flag::retrieveByNameOrCreate(QStringLiteral(AKONADI_FLAG_SEEN)).id());
168 qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("%1 AS %2").arg(PimItemFlagRelation::tableName(), SeenFlagsTableName), seenCondition);
169 }
170 {
171 Query::Condition ignoredCondition(Query::And);
172 ignoredCondition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, FLAGS_COLUMN(IgnoredFlags, leftColumn));
173 ignoredCondition.addValueCondition(FLAGS_COLUMN(IgnoredFlags, rightColumn),
174 Query::Equals,
175 Flag::retrieveByNameOrCreate(QStringLiteral(AKONADI_FLAG_IGNORED)).id());
176 qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("%1 AS %2").arg(PimItemFlagRelation::tableName(), IgnoredFlagsTableName), ignoredCondition);
177 }
178
179#undef FLAGS_COLUMN
180
181 return qb;
182}
183
184CollectionStatistics::Statistics CollectionStatistics::calculateCollectionStatistics(const Collection &col)
185{
186 auto qb = prepareGenericQuery();
187
188 if (col.isVirtual()) {
189 qb.addJoin(QueryBuilder::InnerJoin,
190 CollectionPimItemRelation::tableName(),
191 CollectionPimItemRelation::rightFullColumnName(),
192 PimItem::idFullColumnName());
193 qb.addValueCondition(CollectionPimItemRelation::leftFullColumnName(), Query::Equals, col.id());
194 } else {
195 qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, col.id());
196 }
197
198 if (!qb.exec()) {
199 return {-1, -1, -1};
200 }
201 if (!qb.query().next()) {
202 qCCritical(AKONADISERVER_LOG) << "Error during retrieving result of statistics query:" << qb.query().lastError().text();
203 return {-1, -1, -1};
204 }
205
206 auto result = Statistics{qb.query().value(0).toLongLong(), qb.query().value(1).toLongLong(), qb.query().value(2).toLongLong()};
207 qb.query().finish();
208 return result;
209}
Represents a collection of PIM items.
Definition collection.h:62
Helper class for creating queries to count elements in a database.
Helper class to construct arbitrary SQL queries.
@ InnerJoin
NOTE: only supported for UPDATE and SELECT queries.
@ LeftJoin
NOTE: only supported for SELECT queries.
Represents a WHERE condition tree.
Definition query.h:62
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
void clear()
bool contains(const Key &key) const const
iterator end()
iterator find(const Key &key)
iterator insert(const Key &key, const T &value)
bool remove(const Key &key)
T value(qsizetype i) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:38 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.