File indexing completed on 2024-05-19 16:38:21
0001 /* 0002 SPDX-FileCopyrightText: 2015, 2016 Ivan Cukic <ivan.cukic(at)kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0005 */ 0006 0007 #include "resultset.h" 0008 0009 // Qt 0010 #include <QSqlQuery> 0011 #include <QSqlError> 0012 #include <QCoreApplication> 0013 #include <QDir> 0014 #include <QUrl> 0015 0016 // Local 0017 #include <common/database/Database.h> 0018 #include <common/specialvalues.h> 0019 #include <utils/debug_and_return.h> 0020 #include <utils/qsqlquery_iterator.h> 0021 #include "kactivities-stats-logsettings.h" 0022 0023 // STL 0024 #include <iterator> 0025 #include <functional> 0026 #include <mutex> 0027 0028 // KActivities 0029 #include "activitiessync_p.h" 0030 0031 #define DEBUG_QUERIES 0 0032 0033 namespace KActivities { 0034 namespace Stats { 0035 0036 using namespace Terms; 0037 0038 class ResultSet_ResultPrivate { 0039 public: 0040 QString resource; 0041 QString title; 0042 QString mimetype; 0043 double score; 0044 uint lastUpdate; 0045 uint firstUpdate; 0046 ResultSet::Result::LinkStatus linkStatus; 0047 QStringList linkedActivities; 0048 QString agent; 0049 }; 0050 0051 ResultSet::Result::Result() 0052 : d(new ResultSet_ResultPrivate()) 0053 { 0054 } 0055 0056 ResultSet::Result::Result(Result &&result) 0057 : d(result.d) 0058 { 0059 result.d = nullptr; 0060 } 0061 0062 ResultSet::Result::Result(const Result &result) 0063 : d(new ResultSet_ResultPrivate(*result.d)) 0064 { 0065 } 0066 0067 ResultSet::Result &ResultSet::Result::operator=(Result result) 0068 { 0069 std::swap(d, result.d); 0070 0071 return *this; 0072 } 0073 0074 ResultSet::Result::~Result() 0075 { 0076 delete d; 0077 } 0078 0079 /* clang-format off */ 0080 #define CREATE_GETTER_AND_SETTER(Type, Name, Set) \ 0081 Type ResultSet::Result::Name() const \ 0082 { \ 0083 return d->Name; \ 0084 } \ 0085 \ 0086 void ResultSet::Result::Set(Type Name) \ 0087 { \ 0088 d->Name = Name; \ 0089 } 0090 /* clang-format on */ 0091 0092 CREATE_GETTER_AND_SETTER(QString, resource, setResource) 0093 CREATE_GETTER_AND_SETTER(QString, title, setTitle) 0094 CREATE_GETTER_AND_SETTER(QString, mimetype, setMimetype) 0095 CREATE_GETTER_AND_SETTER(double, score, setScore) 0096 CREATE_GETTER_AND_SETTER(uint, lastUpdate, setLastUpdate) 0097 CREATE_GETTER_AND_SETTER(uint, firstUpdate, setFirstUpdate) 0098 CREATE_GETTER_AND_SETTER(ResultSet::Result::LinkStatus, linkStatus, setLinkStatus) 0099 CREATE_GETTER_AND_SETTER(QStringList, linkedActivities, setLinkedActivities) 0100 CREATE_GETTER_AND_SETTER(QString, agent, setAgent) 0101 0102 #undef CREATE_GETTER_AND_SETTER 0103 0104 QUrl ResultSet::Result::url() const 0105 { 0106 if (QDir::isAbsolutePath(d->resource)) { 0107 return QUrl::fromLocalFile(d->resource); 0108 } else { 0109 return QUrl(d->resource); 0110 } 0111 } 0112 0113 class ResultSetPrivate { 0114 public: 0115 Common::Database::Ptr database; 0116 QSqlQuery query; 0117 Query queryDefinition; 0118 0119 mutable ActivitiesSync::ConsumerPtr activities; 0120 0121 void initQuery() 0122 { 0123 if (!database || query.isActive()) { 0124 return; 0125 } 0126 0127 auto selection = queryDefinition.selection(); 0128 0129 /* clang-format off */ 0130 query = database->execQuery(replaceQueryParameters( 0131 selection == LinkedResources ? linkedResourcesQuery() 0132 : selection == UsedResources ? usedResourcesQuery() 0133 : selection == AllResources ? allResourcesQuery() 0134 : QString())); 0135 /* clang-format on */ 0136 0137 if (query.lastError().isValid()) { 0138 qCWarning(KACTIVITIES_STATS_LOG) << "[Error at ResultSetPrivate::initQuery]: " << query.lastError(); 0139 } 0140 } 0141 0142 QString agentClause(const QString &agent) const 0143 { 0144 if (agent == QLatin1String(":any")) { 0145 return QStringLiteral("1"); 0146 } 0147 0148 return QLatin1String("agent = '") 0149 + Common::escapeSqliteLikePattern(agent == QLatin1String(":current") ? QCoreApplication::instance()->applicationName() : agent) 0150 + QLatin1String("'"); 0151 } 0152 0153 QString activityClause(const QString &activity) const 0154 { 0155 if (activity == QLatin1String(":any")) { 0156 return QStringLiteral("1"); 0157 } 0158 0159 return QLatin1String("activity = '") + // 0160 Common::escapeSqliteLikePattern(activity == QLatin1String(":current") ? ActivitiesSync::currentActivity(activities) : activity) 0161 + QLatin1String("'"); 0162 } 0163 0164 inline QString starPattern(const QString &pattern) const 0165 { 0166 return Common::parseStarPattern(pattern, QStringLiteral("%"), [] (QString str) { 0167 return str.replace(QLatin1String("%"), QLatin1String("\\%")).replace(QLatin1String("_"), QLatin1String("\\_")); 0168 }); 0169 } 0170 0171 QString urlFilterClause(const QString &urlFilter) const 0172 { 0173 if (urlFilter == QLatin1String("*")) { 0174 return QStringLiteral("1"); 0175 } 0176 0177 return QStringLiteral("resource LIKE '") + Common::starPatternToLike(urlFilter) + QStringLiteral("' ESCAPE '\\'"); 0178 } 0179 0180 QString mimetypeClause(const QString &mimetype) const 0181 { 0182 if (mimetype == ANY_TYPE_TAG || mimetype == QLatin1String("*")) { 0183 return QStringLiteral("1"); 0184 0185 } else if (mimetype == FILES_TYPE_TAG) { 0186 return QStringLiteral("mimetype != 'inode/directory' AND mimetype != ''"); 0187 } else if (mimetype == DIRECTORIES_TYPE_TAG) { 0188 return QStringLiteral("mimetype = 'inode/directory'"); 0189 } 0190 0191 return QStringLiteral("mimetype LIKE '") + Common::starPatternToLike(mimetype) + QStringLiteral("' ESCAPE '\\'"); 0192 } 0193 0194 QString dateClause(QDate start, QDate end) const { 0195 if (end.isNull()) { 0196 // only date filtering 0197 return QStringLiteral("DATE(re.start, 'unixepoch') = '") + 0198 start.toString(Qt::ISODate) + QStringLiteral("' "); 0199 } else { 0200 // date range filtering 0201 return QStringLiteral("DATE(re.start, 'unixepoch') >= '") + 0202 start.toString(Qt::ISODate) + QStringLiteral("' AND DATE(re.start, 'unixepoch') <= '") + 0203 end.toString(Qt::ISODate) + QStringLiteral("' "); 0204 } 0205 } 0206 0207 QString resourceEventJoinClause() const { 0208 return QStringLiteral(R"sql( 0209 LEFT JOIN 0210 ResourceEvent re 0211 ON from_table.targettedResource = re.targettedResource 0212 AND from_table.usedActivity = re.usedActivity 0213 AND from_table.initiatingAgent = re.initiatingAgent 0214 )sql"); 0215 } 0216 0217 /** 0218 * Transforms the input list's elements with the f member method, 0219 * and returns the resulting list 0220 */ 0221 template <typename F> 0222 inline 0223 QStringList transformedList(const QStringList &input, F f) const 0224 { 0225 using namespace std::placeholders; 0226 0227 QStringList result; 0228 std::transform(input.cbegin(), input.cend(), 0229 std::back_inserter(result), 0230 std::bind(f, this, _1)); 0231 0232 return result; 0233 } 0234 0235 QString limitOffsetSuffix() const 0236 { 0237 QString result; 0238 0239 const int limit = queryDefinition.limit(); 0240 if (limit > 0) { 0241 result += QStringLiteral(" LIMIT ") + QString::number(limit); 0242 0243 const int offset = queryDefinition.offset(); 0244 if (offset > 0) { 0245 result += QStringLiteral(" OFFSET ") + QString::number(offset); 0246 } 0247 } 0248 0249 return result; 0250 } 0251 0252 inline QString replaceQueryParameters(const QString &_query) const 0253 { 0254 // ORDER BY column 0255 auto ordering = queryDefinition.ordering(); 0256 /* clang-format off */ 0257 QString orderingColumn = QStringLiteral("linkStatus DESC, ") + ( 0258 ordering == HighScoredFirst ? QStringLiteral("score DESC,") 0259 : ordering == RecentlyCreatedFirst ? QStringLiteral("firstUpdate DESC,") 0260 : ordering == RecentlyUsedFirst ? QStringLiteral("lastUpdate DESC,") 0261 : ordering == OrderByTitle ? QStringLiteral("title ASC,") 0262 : QString() 0263 ); 0264 /* clang-format on */ 0265 0266 // WHERE clause for filtering on agents 0267 QStringList agentsFilter = transformedList( 0268 queryDefinition.agents(), &ResultSetPrivate::agentClause); 0269 0270 // WHERE clause for filtering on activities 0271 QStringList activitiesFilter = transformedList( 0272 queryDefinition.activities(), &ResultSetPrivate::activityClause); 0273 0274 // WHERE clause for filtering on resource URLs 0275 QStringList urlFilter = transformedList( 0276 queryDefinition.urlFilters(), &ResultSetPrivate::urlFilterClause); 0277 0278 // WHERE clause for filtering on resource mime 0279 QStringList mimetypeFilter = transformedList( 0280 queryDefinition.types(), &ResultSetPrivate::mimetypeClause); 0281 0282 QString dateColumn = QStringLiteral("1"); 0283 QString resourceEventJoin; 0284 // WHERE clause for access date filtering and ResourceEvent table Join 0285 if (!queryDefinition.dateStart().isNull()) { 0286 dateColumn = dateClause(queryDefinition.dateStart(), queryDefinition.dateEnd()); 0287 0288 resourceEventJoin = resourceEventJoinClause(); 0289 } 0290 0291 auto queryString = _query; 0292 0293 queryString.replace(QStringLiteral("ORDER_BY_CLAUSE"), QStringLiteral("ORDER BY $orderingColumn resource ASC")) 0294 .replace(QStringLiteral("LIMIT_CLAUSE"), limitOffsetSuffix()); 0295 0296 return kamd::utils::debug_and_return(DEBUG_QUERIES, "Query: ", 0297 queryString 0298 .replace(QLatin1String("$orderingColumn"), orderingColumn) 0299 .replace(QLatin1String("$agentsFilter"), agentsFilter.join(QStringLiteral(" OR "))) 0300 .replace(QLatin1String("$activitiesFilter"), activitiesFilter.join(QStringLiteral(" OR "))) 0301 .replace(QLatin1String("$urlFilter"), urlFilter.join(QStringLiteral(" OR "))) 0302 .replace(QLatin1String("$mimetypeFilter"), mimetypeFilter.join(QStringLiteral(" OR "))) 0303 .replace(QLatin1String("$resourceEventJoin"), resourceEventJoin) 0304 .replace(QLatin1String("$dateFilter"), dateColumn) 0305 ); 0306 } 0307 0308 static const QString &linkedResourcesQuery() 0309 { 0310 // TODO: We need to correct the scores based on the time that passed 0311 // since the cache was last updated, although, for this query, 0312 // scores are not that important. 0313 /* clang-format off */ 0314 static const QString queryString = 0315 QStringLiteral(R"sql( 0316 SELECT 0317 from_table.targettedResource as resource 0318 , SUM(rsc.cachedScore) as score 0319 , MIN(rsc.firstUpdate) as firstUpdate 0320 , MAX(rsc.lastUpdate) as lastUpdate 0321 , from_table.usedActivity as activity 0322 , from_table.initiatingAgent as agent 0323 , COALESCE(ri.title, from_table.targettedResource) as title 0324 , ri.mimetype as mimetype 0325 , 2 as linkStatus 0326 0327 FROM 0328 ResourceLink from_table 0329 LEFT JOIN 0330 ResourceScoreCache rsc 0331 ON from_table.targettedResource = rsc.targettedResource 0332 AND from_table.usedActivity = rsc.usedActivity 0333 AND from_table.initiatingAgent = rsc.initiatingAgent 0334 LEFT JOIN 0335 ResourceInfo ri 0336 ON from_table.targettedResource = ri.targettedResource 0337 0338 $resourceEventJoin 0339 0340 WHERE 0341 ($agentsFilter) 0342 AND ($activitiesFilter) 0343 AND ($urlFilter) 0344 AND ($mimetypeFilter) 0345 AND ($dateFilter) 0346 0347 GROUP BY resource, title 0348 0349 ORDER_BY_CLAUSE 0350 LIMIT_CLAUSE 0351 )sql") 0352 ; 0353 /* clang-format on */ 0354 0355 return queryString; 0356 } 0357 0358 static const QString &usedResourcesQuery() 0359 { 0360 // TODO: We need to correct the scores based on the time that passed 0361 // since the cache was last updated 0362 static const QString queryString = 0363 QStringLiteral(R"sql( 0364 SELECT 0365 from_table.targettedResource as resource 0366 , SUM(from_table.cachedScore) as score 0367 , MIN(from_table.firstUpdate) as firstUpdate 0368 , MAX(from_table.lastUpdate) as lastUpdate 0369 , from_table.usedActivity as activity 0370 , from_table.initiatingAgent as agent 0371 , COALESCE(ri.title, from_table.targettedResource) as title 0372 , ri.mimetype as mimetype 0373 , 1 as linkStatus 0374 0375 FROM 0376 ResourceScoreCache from_table 0377 LEFT JOIN 0378 ResourceInfo ri 0379 ON from_table.targettedResource = ri.targettedResource 0380 0381 $resourceEventJoin 0382 0383 WHERE 0384 ($agentsFilter) 0385 AND ($activitiesFilter) 0386 AND ($urlFilter) 0387 AND ($mimetypeFilter) 0388 AND ($dateFilter) 0389 0390 GROUP BY resource, title 0391 0392 ORDER_BY_CLAUSE 0393 LIMIT_CLAUSE 0394 )sql") 0395 ; 0396 0397 return queryString; 0398 } 0399 0400 static const QString &allResourcesQuery() 0401 { 0402 // TODO: We need to correct the scores based on the time that passed 0403 // since the cache was last updated, although, for this query, 0404 // scores are not that important. 0405 static const QString queryString = 0406 QStringLiteral(R"sql( 0407 WITH 0408 LinkedResourcesResults AS ( 0409 SELECT from_table.targettedResource as resource 0410 , rsc.cachedScore as score 0411 , rsc.firstUpdate as firstUpdate 0412 , rsc.lastUpdate as lastUpdate 0413 , from_table.usedActivity as activity 0414 , from_table.initiatingAgent as agent 0415 , 2 as linkStatus 0416 0417 FROM 0418 ResourceLink from_table 0419 0420 LEFT JOIN 0421 ResourceScoreCache rsc 0422 ON from_table.targettedResource = rsc.targettedResource 0423 AND from_table.usedActivity = rsc.usedActivity 0424 AND from_table.initiatingAgent = rsc.initiatingAgent 0425 0426 $resourceEventJoin 0427 0428 WHERE 0429 ($agentsFilter) 0430 AND ($activitiesFilter) 0431 AND ($urlFilter) 0432 AND ($mimetypeFilter) 0433 AND ($dateFilter) 0434 ), 0435 0436 UsedResourcesResults AS ( 0437 SELECT from_table.targettedResource as resource 0438 , from_table.cachedScore as score 0439 , from_table.firstUpdate as firstUpdate 0440 , from_table.lastUpdate as lastUpdate 0441 , from_table.usedActivity as activity 0442 , from_table.initiatingAgent as agent 0443 , 0 as linkStatus 0444 0445 FROM 0446 ResourceScoreCache from_table 0447 0448 $resourceEventJoin 0449 0450 WHERE 0451 ($agentsFilter) 0452 AND ($activitiesFilter) 0453 AND ($urlFilter) 0454 AND ($mimetypeFilter) 0455 AND ($dateFilter) 0456 ), 0457 0458 CollectedResults AS ( 0459 SELECT * 0460 FROM LinkedResourcesResults 0461 0462 UNION 0463 0464 SELECT * 0465 FROM UsedResourcesResults 0466 WHERE resource NOT IN (SELECT resource FROM LinkedResourcesResults) 0467 ) 0468 0469 SELECT 0470 resource 0471 , SUM(score) as score 0472 , MIN(firstUpdate) as firstUpdate 0473 , MAX(lastUpdate) as lastUpdate 0474 , activity 0475 , agent 0476 , COALESCE(ri.title, resource) as title 0477 , ri.mimetype as mimetype 0478 , linkStatus 0479 0480 FROM CollectedResults cr 0481 0482 LEFT JOIN 0483 ResourceInfo ri 0484 ON cr.resource = ri.targettedResource 0485 0486 GROUP BY resource, title 0487 0488 ORDER_BY_CLAUSE 0489 LIMIT_CLAUSE 0490 )sql") 0491 ; 0492 0493 return queryString; 0494 } 0495 0496 ResultSet::Result currentResult() const 0497 { 0498 ResultSet::Result result; 0499 0500 if (!database || !query.isActive()) { 0501 return result; 0502 } 0503 0504 result.setResource(query.value(QStringLiteral("resource")).toString()); 0505 result.setTitle(query.value(QStringLiteral("title")).toString()); 0506 result.setMimetype(query.value(QStringLiteral("mimetype")).toString()); 0507 result.setScore(query.value(QStringLiteral("score")).toDouble()); 0508 result.setLastUpdate(query.value(QStringLiteral("lastUpdate")).toUInt()); 0509 result.setFirstUpdate(query.value(QStringLiteral("firstUpdate")).toUInt()); 0510 result.setAgent(query.value(QStringLiteral("agent")).toString()); 0511 0512 result.setLinkStatus( 0513 static_cast<ResultSet::Result::LinkStatus>(query.value(QStringLiteral("linkStatus")).toUInt())); 0514 0515 auto linkedActivitiesQuery = database->createQuery(); 0516 0517 linkedActivitiesQuery.prepare(QStringLiteral(R"sql( 0518 SELECT usedActivity 0519 FROM ResourceLink 0520 WHERE targettedResource = :resource 0521 )sql")); 0522 0523 linkedActivitiesQuery.bindValue(QStringLiteral(":resource"), result.resource()); 0524 linkedActivitiesQuery.exec(); 0525 0526 QStringList linkedActivities; 0527 for (const auto &item: linkedActivitiesQuery) { 0528 linkedActivities << item[0].toString(); 0529 } 0530 0531 result.setLinkedActivities(linkedActivities); 0532 // qDebug(KACTIVITIES_STATS_LOG) << result.resource() << "linked to activities" << result.linkedActivities(); 0533 0534 return result; 0535 } 0536 }; 0537 0538 ResultSet::ResultSet(Query queryDefinition) 0539 : d(new ResultSetPrivate()) 0540 { 0541 using namespace Common; 0542 0543 d->database = Database::instance(Database::ResourcesDatabase, Database::ReadOnly); 0544 0545 if (!(d->database)) { 0546 qCWarning(KACTIVITIES_STATS_LOG) << "KActivities ERROR: There is no database. This probably means " 0547 "that you do not have the Activity Manager running, or that " 0548 "something else is broken on your system. Recent documents and " 0549 "alike will not work!"; 0550 } 0551 0552 d->queryDefinition = queryDefinition; 0553 0554 d->initQuery(); 0555 } 0556 0557 ResultSet::ResultSet(ResultSet &&source) 0558 : d(nullptr) 0559 { 0560 std::swap(d, source.d); 0561 } 0562 0563 ResultSet::ResultSet(const ResultSet &source) 0564 : d(new ResultSetPrivate(*source.d)) 0565 { 0566 } 0567 0568 ResultSet &ResultSet::operator= (ResultSet source) 0569 { 0570 std::swap(d, source.d); 0571 return *this; 0572 } 0573 0574 ResultSet::~ResultSet() 0575 { 0576 delete d; 0577 } 0578 0579 ResultSet::Result ResultSet::at(int index) const 0580 { 0581 if (!d->query.isActive()) { 0582 return Result(); 0583 } 0584 0585 d->query.seek(index); 0586 0587 return d->currentResult(); 0588 } 0589 0590 } // namespace Stats 0591 } // namespace KActivities 0592 0593 #include "resultset_iterator.cpp" 0594