PlasmaActivitiesStats

resultset.cpp
1/*
2 SPDX-FileCopyrightText: 2015, 2016 Ivan Cukic <ivan.cukic(at)kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6
7#include "resultset.h"
8
9// Qt
10#include <QCoreApplication>
11#include <QDir>
12#include <QSqlError>
13#include <QSqlQuery>
14#include <QUrl>
15
16// Local
17#include "plasma-activities-stats-logsettings.h"
18#include <common/database/Database.h>
19#include <common/specialvalues.h>
20#include <utils/debug_and_return.h>
21#include <utils/qsqlquery_iterator.h>
22
23// STL
24#include <functional>
25#include <iterator>
26#include <mutex>
27
28// KActivities
29#include "activitiessync_p.h"
30
31#define DEBUG_QUERIES 0
32
33namespace KActivities
34{
35namespace Stats
36{
37using namespace Terms;
38
39class ResultSet_ResultPrivate
40{
41public:
42 QString resource;
43 QString title;
44 QString mimetype;
45 double score;
46 uint lastUpdate;
47 uint firstUpdate;
48 ResultSet::Result::LinkStatus linkStatus;
49 QStringList linkedActivities;
50 QString agent;
51};
52
53ResultSet::Result::Result()
54 : d(new ResultSet_ResultPrivate())
55{
56}
57
58ResultSet::Result::Result(Result &&result)
59 : d(result.d)
60{
61 result.d = nullptr;
62}
63
64ResultSet::Result::Result(const Result &result)
65 : d(new ResultSet_ResultPrivate(*result.d))
66{
67}
68
69ResultSet::Result &ResultSet::Result::operator=(Result result)
70{
71 std::swap(d, result.d);
72
73 return *this;
74}
75
76ResultSet::Result::~Result()
77{
78 delete d;
79}
80
81#define CREATE_GETTER_AND_SETTER(Type, Name, Set) \
82 Type ResultSet::Result::Name() const \
83 { \
84 return d->Name; \
85 } \
86 \
87 void ResultSet::Result::Set(Type Name) \
88 { \
89 d->Name = Name; \
90 }
91
92CREATE_GETTER_AND_SETTER(QString, resource, setResource)
93CREATE_GETTER_AND_SETTER(QString, title, setTitle)
94CREATE_GETTER_AND_SETTER(QString, mimetype, setMimetype)
95CREATE_GETTER_AND_SETTER(double, score, setScore)
96CREATE_GETTER_AND_SETTER(uint, lastUpdate, setLastUpdate)
97CREATE_GETTER_AND_SETTER(uint, firstUpdate, setFirstUpdate)
98CREATE_GETTER_AND_SETTER(ResultSet::Result::LinkStatus, linkStatus, setLinkStatus)
99CREATE_GETTER_AND_SETTER(QStringList, linkedActivities, setLinkedActivities)
100CREATE_GETTER_AND_SETTER(QString, agent, setAgent)
101
102#undef CREATE_GETTER_AND_SETTER
103
105{
106 if (QDir::isAbsolutePath(d->resource)) {
107 return QUrl::fromLocalFile(d->resource);
108 } else {
109 return QUrl(d->resource);
110 }
111}
112
113class ResultSetPrivate
114{
115public:
116 Common::Database::Ptr database;
117 QSqlQuery query;
118 Query queryDefinition;
119
120 mutable ActivitiesSync::ConsumerPtr activities;
121
122 void initQuery()
123 {
124 if (!database || query.isActive()) {
125 return;
126 }
127
128 auto selection = queryDefinition.selection();
129
130 query = database->execQuery(replaceQueryParameters( //
131 selection == LinkedResources ? linkedResourcesQuery()
132 : selection == UsedResources ? usedResourcesQuery()
133 : selection == AllResources ? allResourcesQuery()
134 : QString()));
135
136 if (query.lastError().isValid()) {
137 qCWarning(PLASMA_ACTIVITIES_STATS_LOG) << "[Error at ResultSetPrivate::initQuery]: " << query.lastError();
138 }
139 }
140
141 QString agentClause(const QString &agent) const
142 {
143 if (agent == QLatin1String(":any")) {
144 return QStringLiteral("1");
145 }
146
147 return QLatin1String("agent = '")
148 + Common::escapeSqliteLikePattern(agent == QLatin1String(":current") ? QCoreApplication::instance()->applicationName() : agent)
149 + QLatin1String("'");
150 }
151
152 QString activityClause(const QString &activity) const
153 {
154 if (activity == QLatin1String(":any")) {
155 return QStringLiteral("1");
156 }
157
158 return QLatin1String("activity = '") + //
159 Common::escapeSqliteLikePattern(activity == QLatin1String(":current") ? ActivitiesSync::currentActivity(activities) : activity)
160 + QLatin1String("'");
161 }
162
163 inline QString starPattern(const QString &pattern) const
164 {
165 return Common::parseStarPattern(pattern, QStringLiteral("%"), [](QString str) {
166 return str.replace(QLatin1String("%"), QLatin1String("\\%")).replace(QLatin1String("_"), QLatin1String("\\_"));
167 });
168 }
169
170 QString urlFilterClause(const QString &urlFilter) const
171 {
172 if (urlFilter == QLatin1String("*")) {
173 return QStringLiteral("1");
174 }
175
176 return QLatin1String("resource LIKE '") + Common::starPatternToLike(urlFilter) + QLatin1String("' ESCAPE '\\'");
177 }
178
179 QString mimetypeClause(const QString &mimetype) const
180 {
181 if (mimetype == ANY_TYPE_TAG || mimetype == QLatin1String("*")) {
182 return QStringLiteral("1");
183
184 } else if (mimetype == FILES_TYPE_TAG) {
185 return QStringLiteral("mimetype != 'inode/directory' AND mimetype != ''");
186 } else if (mimetype == DIRECTORIES_TYPE_TAG) {
187 return QStringLiteral("mimetype = 'inode/directory'");
188 }
189
190 return QLatin1String("mimetype LIKE '") + Common::starPatternToLike(mimetype) + QLatin1String("' ESCAPE '\\'");
191 }
192
193 QString dateClause(QDate start, QDate end) const
194 {
195 if (end.isNull()) {
196 // only date filtering
197 return QLatin1String("DATE(re.start, 'unixepoch') = '") + start.toString(Qt::ISODate) + QLatin1String("' ");
198 } else {
199 // date range filtering
200 return QLatin1String("DATE(re.start, 'unixepoch') >= '") + start.toString(Qt::ISODate) + QLatin1String("' AND DATE(re.start, 'unixepoch') <= '")
201 + end.toString(Qt::ISODate) + QLatin1String("' ");
202 }
203 }
204 QString titleClause(const QString titleFilter) const
205 {
206 if (titleFilter == QLatin1String("*")) {
207 return QStringLiteral("1");
208 }
209
210 return QLatin1String("title LIKE '") + Common::starPatternToLike(titleFilter) + QLatin1String("' ESCAPE '\\'");
211 }
212
213 QString resourceEventJoinClause() const
214 {
215 return QStringLiteral(R"(
216 LEFT JOIN
217 ResourceEvent re
218 ON from_table.targettedResource = re.targettedResource
219 AND from_table.usedActivity = re.usedActivity
220 AND from_table.initiatingAgent = re.initiatingAgent
221 )");
222 }
223
224 /**
225 * Transforms the input list's elements with the f member method,
226 * and returns the resulting list
227 */
228 template<typename F>
229 inline QStringList transformedList(const QStringList &input, F f) const
230 {
231 using namespace std::placeholders;
232
233 QStringList result;
234 std::transform(input.cbegin(), input.cend(), std::back_inserter(result), std::bind(f, this, _1));
235
236 return result;
237 }
238
239 QString limitOffsetSuffix() const
240 {
241 QString result;
242
243 const int limit = queryDefinition.limit();
244 if (limit > 0) {
245 result += QLatin1String(" LIMIT ") + QString::number(limit);
246
247 const int offset = queryDefinition.offset();
248 if (offset > 0) {
249 result += QLatin1String(" OFFSET ") + QString::number(offset);
250 }
251 }
252
253 return result;
254 }
255
256 inline QString replaceQueryParameters(const QString &_query) const
257 {
258 // ORDER BY column
259 auto ordering = queryDefinition.ordering();
260 QString orderingColumn = QLatin1String("linkStatus DESC, ")
261 + (ordering == HighScoredFirst ? QLatin1String("score DESC,")
262 : ordering == RecentlyCreatedFirst ? QLatin1String("firstUpdate DESC,")
263 : ordering == RecentlyUsedFirst ? QLatin1String("lastUpdate DESC,")
264 : ordering == OrderByTitle ? QLatin1String("title ASC,")
265 : QLatin1String());
266
267 // WHERE clause for filtering on agents
268 QStringList agentsFilter = transformedList(queryDefinition.agents(), &ResultSetPrivate::agentClause);
269
270 // WHERE clause for filtering on activities
271 QStringList activitiesFilter = transformedList(queryDefinition.activities(), &ResultSetPrivate::activityClause);
272
273 // WHERE clause for filtering on resource URLs
274 QStringList urlFilter = transformedList(queryDefinition.urlFilters(), &ResultSetPrivate::urlFilterClause);
275
276 // WHERE clause for filtering on resource mime
277 QStringList mimetypeFilter = transformedList(queryDefinition.types(), &ResultSetPrivate::mimetypeClause);
278 QStringList titleFilter = transformedList(queryDefinition.titleFilters(), &ResultSetPrivate::titleClause);
279
280 QString dateColumn = QStringLiteral("1");
281 QString resourceEventJoin;
282 // WHERE clause for access date filtering and ResourceEvent table Join
283 if (!queryDefinition.dateStart().isNull()) {
284 dateColumn = dateClause(queryDefinition.dateStart(), queryDefinition.dateEnd());
285
286 resourceEventJoin = resourceEventJoinClause();
287 }
288
289 auto queryString = _query;
290
291 queryString.replace(QLatin1String("ORDER_BY_CLAUSE"), QLatin1String("ORDER BY $orderingColumn resource ASC"))
292 .replace(QLatin1String("LIMIT_CLAUSE"), limitOffsetSuffix());
293
294 const QString replacedQuery =
295 queryString.replace(QLatin1String("$orderingColumn"), orderingColumn)
296 .replace(QLatin1String("$agentsFilter"), agentsFilter.join(QStringLiteral(" OR ")))
297 .replace(QLatin1String("$activitiesFilter"), activitiesFilter.join(QStringLiteral(" OR ")))
298 .replace(QLatin1String("$urlFilter"), urlFilter.join(QStringLiteral(" OR ")))
299 .replace(QLatin1String("$mimetypeFilter"), mimetypeFilter.join(QStringLiteral(" OR ")))
300 .replace(QLatin1String("$resourceEventJoin"), resourceEventJoin)
301 .replace(QLatin1String("$dateFilter"), dateColumn)
302 .replace(QLatin1String("$titleFilter"), titleFilter.isEmpty() ? QStringLiteral("1") : titleFilter.join(QStringLiteral(" OR ")));
303 return kamd::utils::debug_and_return(DEBUG_QUERIES, "Query: ", replacedQuery);
304 }
305
306 static const QString &linkedResourcesQuery()
307 {
308 // TODO: We need to correct the scores based on the time that passed
309 // since the cache was last updated, although, for this query,
310 // scores are not that important.
311 static const QString queryString = QStringLiteral(R"(
312 SELECT
313 from_table.targettedResource as resource
314 , SUM(rsc.cachedScore) as score
315 , MIN(rsc.firstUpdate) as firstUpdate
316 , MAX(rsc.lastUpdate) as lastUpdate
317 , from_table.usedActivity as activity
318 , from_table.initiatingAgent as agent
319 , COALESCE(ri.title, from_table.targettedResource) as title
320 , ri.mimetype as mimetype
321 , 2 as linkStatus
322
323 FROM
324 ResourceLink from_table
325 LEFT JOIN
326 ResourceScoreCache rsc
327 ON from_table.targettedResource = rsc.targettedResource
328 AND from_table.usedActivity = rsc.usedActivity
329 AND from_table.initiatingAgent = rsc.initiatingAgent
330 LEFT JOIN
331 ResourceInfo ri
332 ON from_table.targettedResource = ri.targettedResource
333
334 $resourceEventJoin
335
336 WHERE
337 ($agentsFilter)
338 AND ($activitiesFilter)
339 AND ($urlFilter)
340 AND ($mimetypeFilter)
341 AND ($dateFilter)
342 AND ($titleFilter)
343
344 GROUP BY resource, title
345
346 ORDER_BY_CLAUSE
347 LIMIT_CLAUSE
348 )");
349
350 return queryString;
351 }
352
353 static const QString &usedResourcesQuery()
354 {
355 // TODO: We need to correct the scores based on the time that passed
356 // since the cache was last updated
357 static const QString queryString = QStringLiteral(R"(
358 SELECT
359 from_table.targettedResource as resource
360 , SUM(from_table.cachedScore) as score
361 , MIN(from_table.firstUpdate) as firstUpdate
362 , MAX(from_table.lastUpdate) as lastUpdate
363 , from_table.usedActivity as activity
364 , from_table.initiatingAgent as agent
365 , COALESCE(ri.title, from_table.targettedResource) as title
366 , ri.mimetype as mimetype
367 , 1 as linkStatus
368
369 FROM
370 ResourceScoreCache from_table
371 LEFT JOIN
372 ResourceInfo ri
373 ON from_table.targettedResource = ri.targettedResource
374
375 $resourceEventJoin
376
377 WHERE
378 ($agentsFilter)
379 AND ($activitiesFilter)
380 AND ($urlFilter)
381 AND ($mimetypeFilter)
382 AND ($dateFilter)
383 AND ($titleFilter)
384
385 GROUP BY resource, title
386
387 ORDER_BY_CLAUSE
388 LIMIT_CLAUSE
389 )");
390
391 return queryString;
392 }
393
394 static const QString &allResourcesQuery()
395 {
396 // TODO: We need to correct the scores based on the time that passed
397 // since the cache was last updated, although, for this query,
398 // scores are not that important.
399 static const QString queryString = QStringLiteral(R"(
400 WITH
401 LinkedResourcesResults AS (
402 SELECT from_table.targettedResource as resource
403 , rsc.cachedScore as score
404 , rsc.firstUpdate as firstUpdate
405 , rsc.lastUpdate as lastUpdate
406 , from_table.usedActivity as activity
407 , from_table.initiatingAgent as agent
408 , 2 as linkStatus
409
410 FROM
411 ResourceLink from_table
412
413 LEFT JOIN
414 ResourceScoreCache rsc
415 ON from_table.targettedResource = rsc.targettedResource
416 AND from_table.usedActivity = rsc.usedActivity
417 AND from_table.initiatingAgent = rsc.initiatingAgent
418
419 $resourceEventJoin
420
421 WHERE
422 ($agentsFilter)
423 AND ($activitiesFilter)
424 AND ($urlFilter)
425 AND ($mimetypeFilter)
426 AND ($dateFilter)
427 AND ($titleFilter)
428 ),
429
430 UsedResourcesResults AS (
431 SELECT from_table.targettedResource as resource
432 , from_table.cachedScore as score
433 , from_table.firstUpdate as firstUpdate
434 , from_table.lastUpdate as lastUpdate
435 , from_table.usedActivity as activity
436 , from_table.initiatingAgent as agent
437 , 0 as linkStatus
438
439 FROM
440 ResourceScoreCache from_table
441
442 $resourceEventJoin
443
444 WHERE
445 ($agentsFilter)
446 AND ($activitiesFilter)
447 AND ($urlFilter)
448 AND ($mimetypeFilter)
449 AND ($dateFilter)
450 AND ($titleFilter)
451 ),
452
453 CollectedResults AS (
454 SELECT *
455 FROM LinkedResourcesResults
456
457 UNION
458
459 SELECT *
460 FROM UsedResourcesResults
461 WHERE resource NOT IN (SELECT resource FROM LinkedResourcesResults)
462 )
463
464 SELECT
465 resource
466 , SUM(score) as score
467 , MIN(firstUpdate) as firstUpdate
468 , MAX(lastUpdate) as lastUpdate
469 , activity
470 , agent
471 , COALESCE(ri.title, resource) as title
472 , ri.mimetype as mimetype
473 , linkStatus
474
475 FROM CollectedResults cr
476
477 LEFT JOIN
478 ResourceInfo ri
479 ON cr.resource = ri.targettedResource
480
481 GROUP BY resource, title
482
483 ORDER_BY_CLAUSE
484 LIMIT_CLAUSE
485 )");
486
487 return queryString;
488 }
489
490 ResultSet::Result currentResult() const
491 {
492 ResultSet::Result result;
493
494 if (!database || !query.isActive()) {
495 return result;
496 }
497
498 result.setResource(query.value(QStringLiteral("resource")).toString());
499 result.setTitle(query.value(QStringLiteral("title")).toString());
500 result.setMimetype(query.value(QStringLiteral("mimetype")).toString());
501 result.setScore(query.value(QStringLiteral("score")).toDouble());
502 result.setLastUpdate(query.value(QStringLiteral("lastUpdate")).toUInt());
503 result.setFirstUpdate(query.value(QStringLiteral("firstUpdate")).toUInt());
504 result.setAgent(query.value(QStringLiteral("agent")).toString());
505
506 result.setLinkStatus(static_cast<ResultSet::Result::LinkStatus>(query.value(QStringLiteral("linkStatus")).toUInt()));
507
508 auto linkedActivitiesQuery = database->createQuery();
509
510 linkedActivitiesQuery.prepare(QStringLiteral(R"(
511 SELECT usedActivity
512 FROM ResourceLink
513 WHERE targettedResource = :resource
514 )"));
515
516 linkedActivitiesQuery.bindValue(QStringLiteral(":resource"), result.resource());
517 linkedActivitiesQuery.exec();
518
519 QStringList linkedActivities;
520 for (const auto &item : linkedActivitiesQuery) {
521 linkedActivities << item[0].toString();
522 }
523
524 result.setLinkedActivities(linkedActivities);
525 // qDebug(PLASMA_ACTIVITIES_STATS_LOG) << result.resource() << "linked to activities" << result.linkedActivities();
526
527 return result;
528 }
529};
530
532 : d(new ResultSetPrivate())
533{
534 using namespace Common;
535
536 d->database = Database::instance(Database::ResourcesDatabase, Database::ReadOnly);
537
538 if (!(d->database)) {
539 qCWarning(PLASMA_ACTIVITIES_STATS_LOG) << "Plasma Activities ERROR: There is no database. This probably means "
540 "that you do not have the Activity Manager running, or that "
541 "something else is broken on your system. Recent documents and "
542 "alike will not work!";
543 }
544
545 d->queryDefinition = queryDefinition;
546
547 d->initQuery();
548}
549
551 : d(nullptr)
552{
553 std::swap(d, source.d);
554}
555
556ResultSet::~ResultSet()
557{
558 delete d;
559}
560
562{
563 if (!d->query.isActive()) {
564 return Result();
565 }
566
567 d->query.seek(index);
568
569 return d->currentResult();
570}
571
572} // namespace Stats
573} // namespace KActivities
574
575#include "resultset_iterator.cpp"
The activities system tracks resources (documents, contacts, etc.) that the user has used.
Definition query.h:54
Structure containing data of one of the results.
Definition resultset.h:50
QUrl url() const
Url representation of a resource based on internal resource, readonly,.
Class that can query the KActivities usage tracking mechanism for resources.
Definition resultset.h:44
Result at(int index) const
ResultSet(Query query)
Creates the ResultSet from the specified query.
Q_SCRIPTABLE Q_NOREPLY void start()
char * toString(const EngineQuery &query)
Provides enums and strucss to use.for building queries with Query.
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
QCoreApplication * instance()
bool isNull() const const
bool isAbsolutePath(const QString &path)
const_iterator cbegin() const const
const_iterator cend() const const
bool isEmpty() const const
T value(qsizetype i) const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString join(QChar separator) const const
QUrl fromLocalFile(const QString &localFile)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 11 2025 11:48:19 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.