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

KDE's Doxygen guidelines are available online.