1 /*
2  *   SPDX-FileCopyrightText: 2014-2016 Ivan Cukic <ivan.cukic@kde.org>
3  *
4  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5  */
6 
7 #include "Database.h"
8 
9 #include <common/database/schema/ResourcesDatabaseSchema.h>
10 #include <utils/d_ptr_implementation.h>
11 
12 #include <QDebug>
13 #include <QSqlDatabase>
14 #include <QSqlDriver>
15 #include <QSqlError>
16 #include <QSqlField>
17 #include <QThread>
18 
19 #include <map>
20 #include <mutex>
21 
22 #include "DebugResources.h"
23 
24 namespace Common
25 {
26 namespace
27 {
28 #ifdef QT_DEBUG
29 QString lastExecutedQuery;
30 #endif
31 
32 std::mutex databases_mutex;
33 
34 struct DatabaseInfo {
35     Qt::HANDLE thread;
36     Database::OpenMode openMode;
37 };
38 
operator <(const DatabaseInfo & left,const DatabaseInfo & right)39 bool operator<(const DatabaseInfo &left, const DatabaseInfo &right)
40 {
41     return left.thread < right.thread ? true : left.thread > right.thread ? false : left.openMode < right.openMode;
42 }
43 
44 std::map<DatabaseInfo, std::weak_ptr<Database>> databases;
45 }
46 
47 class QSqlDatabaseWrapper
48 {
49 private:
50     QSqlDatabase m_database;
51     bool m_open;
52     QString m_connectionName;
53 
54 public:
QSqlDatabaseWrapper(const DatabaseInfo & info)55     QSqlDatabaseWrapper(const DatabaseInfo &info)
56         : m_open(false)
57     {
58         m_connectionName = "kactivities_db_resources_"
59             // Adding the thread number to the database name
60             + QString::number((quintptr)info.thread)
61             // And whether it is read-only or read-write
62             + (info.openMode == Database::ReadOnly ? "_readonly" : "_readwrite");
63 
64         m_database = QSqlDatabase::contains(m_connectionName) ? QSqlDatabase::database(m_connectionName)
65                                                               : QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
66 
67         if (info.openMode == Database::ReadOnly) {
68             m_database.setConnectOptions(QStringLiteral("QSQLITE_OPEN_READONLY"));
69         }
70 
71         // We are allowing the database file to be overridden mostly for testing purposes
72         m_database.setDatabaseName(ResourcesDatabaseSchema::path());
73 
74         m_open = m_database.open();
75 
76         if (!m_open) {
77             qCWarning(KAMD_LOG_RESOURCES) << "KActivities: Database is not open: " << m_database.connectionName() << m_database.databaseName()
78                                           << m_database.lastError();
79 
80             if (info.openMode == Database::ReadWrite) {
81                 qFatal("KActivities: Opening the database in RW mode should always succeed");
82             }
83         }
84     }
85 
~QSqlDatabaseWrapper()86     ~QSqlDatabaseWrapper()
87     {
88         qCDebug(KAMD_LOG_RESOURCES) << "Closing SQL connection: " << m_connectionName;
89     }
90 
close()91     void close()
92     {
93         m_database.close();
94     }
95 
get()96     QSqlDatabase &get()
97     {
98         return m_database;
99     }
100 
isOpen() const101     bool isOpen() const
102     {
103         return m_open;
104     }
105 
connectionName() const106     QString connectionName() const
107     {
108         return m_connectionName;
109     }
110 };
111 
112 class Database::Private
113 {
114 public:
Private()115     Private()
116     {
117     }
118 
query(const QString & query)119     QSqlQuery query(const QString &query)
120     {
121         return database ? QSqlQuery(query, database->get()) : QSqlQuery();
122     }
123 
query()124     QSqlQuery query()
125     {
126         return database ? QSqlQuery(database->get()) : QSqlQuery();
127     }
128 
129     QScopedPointer<QSqlDatabaseWrapper> database;
130 };
131 
Locker(Database & database)132 Database::Locker::Locker(Database &database)
133     : m_database(database.d->database->get())
134 {
135     m_database.transaction();
136 }
137 
~Locker()138 Database::Locker::~Locker()
139 {
140     m_database.commit();
141 }
142 
instance(Source source,OpenMode openMode)143 Database::Ptr Database::instance(Source source, OpenMode openMode)
144 {
145     Q_UNUSED(source) // for the time being
146 
147     std::lock_guard<std::mutex> lock(databases_mutex);
148 
149     // We are saving instances per thread and per read/write mode
150     DatabaseInfo info;
151     info.thread = QThread::currentThreadId();
152     info.openMode = openMode;
153 
154     // Do we have an instance matching the request?
155     auto search = databases.find(info);
156     if (search != databases.end()) {
157         auto ptr = search->second.lock();
158 
159         if (ptr) {
160             return ptr;
161         }
162     }
163 
164     // Creating a new database instance
165     auto ptr = std::make_shared<Database>();
166 
167     ptr->d->database.reset(new QSqlDatabaseWrapper(info));
168 
169     if (!ptr->d->database->isOpen()) {
170         return nullptr;
171     }
172 
173     databases[info] = ptr;
174 
175     if (info.openMode == ReadOnly) {
176         // From now on, only SELECT queries will work
177         ptr->setPragma(QStringLiteral("query_only = 1"));
178 
179         // These should not make any difference
180         ptr->setPragma(QStringLiteral("synchronous = 0"));
181 
182     } else {
183         // Using the write-ahead log and sync = NORMAL for faster writes
184         ptr->setPragma(QStringLiteral("synchronous = 1"));
185     }
186 
187     // Maybe we should use the write-ahead log
188     auto walResult = ptr->pragma(QStringLiteral("journal_mode = WAL"));
189 
190     if (walResult != "wal") {
191         qCWarning(KAMD_LOG_RESOURCES) << "KActivities: Database can not be opened in WAL mode. Check the "
192                                          "SQLite version (required >3.7.0). And whether your filesystem "
193                                          "supports shared memory";
194 
195         ptr->d->database->close();
196 
197         return nullptr;
198     }
199 
200     // We don't have a big database, lets flush the WAL when
201     // it reaches 400k, not 4M as is default
202     ptr->setPragma(QStringLiteral("wal_autocheckpoint = 100"));
203 
204     qCDebug(KAMD_LOG_RESOURCES) << "KActivities: Database connection: " << ptr->d->database->connectionName()
205                                 << "\n    query_only:         " << ptr->pragma(QStringLiteral("query_only"))
206                                 << "\n    journal_mode:       " << ptr->pragma(QStringLiteral("journal_mode"))
207                                 << "\n    wal_autocheckpoint: " << ptr->pragma(QStringLiteral("wal_autocheckpoint"))
208                                 << "\n    synchronous:        " << ptr->pragma(QStringLiteral("synchronous"));
209 
210     return ptr;
211 }
212 
Database()213 Database::Database()
214 {
215 }
216 
~Database()217 Database::~Database()
218 {
219 }
220 
createQuery() const221 QSqlQuery Database::createQuery() const
222 {
223     return d->query();
224 }
225 
reportError(const QSqlError & error_)226 void Database::reportError(const QSqlError &error_)
227 {
228     Q_EMIT error(error_);
229 }
230 
lastQuery() const231 QString Database::lastQuery() const
232 {
233 #ifdef QT_DEBUG
234     return lastExecutedQuery;
235 #endif
236     return QString();
237 }
238 
execQuery(const QString & query,bool ignoreErrors) const239 QSqlQuery Database::execQuery(const QString &query, bool ignoreErrors) const
240 {
241     Q_UNUSED(ignoreErrors);
242 #ifdef QT_NO_DEBUG
243     auto result = d->query(query);
244 
245     if (!ignoreErrors && result.lastError().isValid()) {
246         Q_EMIT error(result.lastError());
247     }
248 
249     return result;
250 #else
251     auto result = d->query(query);
252 
253     lastExecutedQuery = query;
254 
255     if (!ignoreErrors && result.lastError().isValid()) {
256         qCWarning(KAMD_LOG_RESOURCES) << "SQL: "
257                                       << "\n    error: " << result.lastError() << "\n    query: " << query;
258     }
259 
260     return result;
261 #endif
262 }
263 
execQueries(const QStringList & queries) const264 QSqlQuery Database::execQueries(const QStringList &queries) const
265 {
266     QSqlQuery result;
267 
268     for (const auto &query : queries) {
269         result = execQuery(query);
270     }
271 
272     return result;
273 }
274 
setPragma(const QString & pragma)275 void Database::setPragma(const QString &pragma)
276 {
277     execQuery(QStringLiteral("PRAGMA ") + pragma);
278 }
279 
pragma(const QString & pragma) const280 QVariant Database::pragma(const QString &pragma) const
281 {
282     return value("PRAGMA " + pragma);
283 }
284 
value(const QString & query) const285 QVariant Database::value(const QString &query) const
286 {
287     auto result = execQuery(query);
288     return result.next() ? result.value(0) : QVariant();
289 }
290 
291 } // namespace Common
292