Akonadi

server/storage/collectionstatistics.cpp
1 /*
2  * SPDX-FileCopyrightText: 2014 Daniel Vrátil <[email protected]>
3  * SPDX-FileCopyrightText: 2016 Daniel Vrátil <[email protected]>
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 
18 using namespace Akonadi::Server;
19 
20 CollectionStatistics::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 
75 void 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 
92 void 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 
107 void 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 
117 void CollectionStatistics::expireCache()
118 {
119  QMutexLocker lock(&mCacheLock);
120  mCache.clear();
121 }
122 
123 CollectionStatistics::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 
133 QueryBuilder 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 std::move(qb);
182 }
183 
184 CollectionStatistics::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 }
@ InnerJoin
NOTE: only supported for UPDATE and SELECT queries.
Definition: querybuilder.h:48
QHash::iterator find(const Key &key)
void clear()
Definition: item.h:32
@ LeftJoin
NOTE: only supported for SELECT queries.
Definition: querybuilder.h:50
QHash::iterator insert(const Key &key, const T &value)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
Helper class for creating queries to count elements in a database.
int remove(const Key &key)
Represents a WHERE condition tree.
Definition: query.h:61
bool contains(const Key &key) const const
T value(int i) const const
Helper class to construct arbitrary SQL queries.
Definition: querybuilder.h:31
QHash::iterator end()
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sat Jun 25 2022 06:00:31 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.