Futuresql

threadeddatabase.cpp
1// SPDX-FileCopyrightText: 2022 Jonah BrĂ¼chert <jbb@kaidan.im>
2//
3// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
4
5#include "threadeddatabase.h"
6#include "threadeddatabase_p.h"
7
8#include <QDir>
9#include <QSqlDatabase>
10#include <QSqlQuery>
11#include <QUrl>
12#include <QStringBuilder>
13#include <QVariant>
14#include <QSqlResult>
15#include <QSqlError>
16#include <QLoggingCategory>
17
18#include <unordered_map>
19
20#define SCHAMA_MIGRATIONS_TABLE "__qt_schema_migrations"
21
22Q_DECLARE_LOGGING_CATEGORY(asyncdatabase)
23Q_LOGGING_CATEGORY(asyncdatabase, "futuresql")
24
25namespace asyncdatabase_private {
26
27// migrations
28void createInternalTable(QSqlDatabase &database) {
29 QSqlQuery query(QStringLiteral("create table if not exists " SCHAMA_MIGRATIONS_TABLE " ("
30 "version Text primary key not null, "
31 "run_on timestamp not null default current_timestamp)"), database);
32 if (!query.exec()) {
33 printSqlError(query);
34 }
35}
36
37void markMigrationRun(QSqlDatabase &database, const QString &name) {
38 qCDebug(asyncdatabase) << "Marking migration" << name << "as done.";
39
40 QSqlQuery query(database);
41 if (!query.prepare(QStringLiteral("insert into " SCHAMA_MIGRATIONS_TABLE " (version) values (:name)"))) {
42 printSqlError(query);
43 }
44 query.bindValue(QStringLiteral(":name"), name);
45 if (!query.exec()) {
46 printSqlError(query);
47 }
48}
49
50QString currentDatabaseVersion(QSqlDatabase &database) {
51 QSqlQuery query(database);
52 query.prepare(QStringLiteral("select version from " SCHAMA_MIGRATIONS_TABLE " order by version desc limit 1"));
53 query.exec();
54
55 if (query.next()) {
56 return query.value(0).toString();
57 } else {
58 return {};
59 }
60}
61
62void runDatabaseMigrations(QSqlDatabase &database, const QString &migrationDirectory)
63{
64 createInternalTable(database);
65
66 QDir dir(migrationDirectory);
67 if (!dir.exists()) {
68 qCWarning(asyncdatabase) << "The migrations directory" << migrationDirectory
69 << "does not exist";
70 }
71 const auto entries = dir.entryList(QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot, QDir::SortFlag::Name);
72
73 const QString currentVersion = currentDatabaseVersion(database);
74 for (const auto &entry : entries) {
75 QDir subdir(entry);
76 if (subdir.dirName() > currentVersion) {
77 QFile file(migrationDirectory % QDir::separator() % entry % QDir::separator() % u"up.sql");
78 if (!file.open(QFile::ReadOnly)) {
79 qCDebug(asyncdatabase) << "Failed to open migration file" << file.fileName();
80 }
81 qCDebug(asyncdatabase) << "Running migration" << subdir.dirName();
82
83 database.transaction();
84
85 // Hackish
86 const auto statements = file.readAll().split(';');
87
88 bool migrationSuccessful = true;
89 for (const QByteArray &statement : statements) {
90 const auto trimmedStatement = QString::fromUtf8(statement.trimmed());
91 QSqlQuery query(database);
92
93 if (!trimmedStatement.isEmpty()) {
94 qCDebug(asyncdatabase) << "Running" << trimmedStatement;
95 if (!query.prepare(trimmedStatement)) {
96 printSqlError(query);
97 migrationSuccessful = false;
98 } else {
99 bool success = query.exec();
100 migrationSuccessful &= success;
101 if (!success) {
102 printSqlError(query);
103 }
104 }
105 }
106 }
107
108 if (migrationSuccessful) {
109 database.commit();
110 markMigrationRun(database, subdir.dirName());
111 } else {
112 qCWarning(asyncdatabase) << "Migration" << subdir.dirName() << "failed, retrying next time.";
113 qCWarning(asyncdatabase) << "Stopping migrations here, as the next migration may depens on this one.";
114 database.rollback();
115 return;
116 }
117 }
118 }
119 qCDebug(asyncdatabase) << "Migrations finished";
120}
121
122struct AsyncSqlDatabasePrivate {
123 QSqlDatabase database;
124 std::unordered_map<QString, QSqlQuery> preparedQueryCache;
125};
126
127// Internal asynchronous database class
128QFuture<void> AsyncSqlDatabase::establishConnection(const DatabaseConfiguration &configuration)
129{
130 return runAsync([=, this] {
131 d->database = QSqlDatabase::addDatabase(configuration.type());
132 if (configuration.databaseName()) {
133 d->database.setDatabaseName(*configuration.databaseName());
134 }
135 if (configuration.hostName()) {
136 d->database.setHostName(*configuration.hostName());
137 }
138 if (configuration.userName()) {
139 d->database.setUserName(*configuration.userName());
140 }
141 if (configuration.password()) {
142 d->database.setPassword(*configuration.password());
143 }
144
145 if (!d->database.open()) {
146 qCDebug(asyncdatabase) << "Failed to open database" << d->database.lastError().text();
147 if (configuration.databaseName()) {
148 qCDebug(asyncdatabase) << "Tried to use database" << *configuration.databaseName();
149 }
150 }
151 });
152}
153
154auto AsyncSqlDatabase::runMigrations(const QString &migrationDirectory) -> QFuture<void> {
155 return runAsync([=, this] {
156 runDatabaseMigrations(d->database, migrationDirectory);
157 });
158}
159auto AsyncSqlDatabase::setCurrentMigrationLevel(const QString &migrationName) -> QFuture<void> {
160 return runAsync([=, this] {
161 createInternalTable(d->database);
162 markMigrationRun(d->database, migrationName);
163 });
164}
165
166AsyncSqlDatabase::AsyncSqlDatabase()
167 : QObject()
168 , d(std::make_unique<AsyncSqlDatabasePrivate>())
169{
170}
171
172AsyncSqlDatabase::~AsyncSqlDatabase() {
173 runAsync([db = d->database] {
174 QSqlDatabase::removeDatabase(db.databaseName());
175 });
176};
177
178Row AsyncSqlDatabase::retrieveRow(const QSqlQuery &query) {
179 Row row;
180 int i = 0;
181
182 while (true) {
183 if (query.isValid()) {
184 QVariant value = query.value(i);
185 if (value.isValid()) {
186 row.push_back(std::move(value));
187 i++;
188 } else {
189 break;
190 }
191 } else {
192 break;
193 }
194 }
195 return row;
196}
197
198Rows AsyncSqlDatabase::retrieveRows(QSqlQuery &query)
199{
200 Rows rows;
201 while (query.next()) {
202 rows.push_back(retrieveRow(query));
203 }
204
205 return rows;
206}
207
208std::optional<Row> AsyncSqlDatabase::retrieveOptionalRow(QSqlQuery &query)
209{
210 query.next();
211
212 if (query.isValid()) {
213 return retrieveRow(query);
214 } else {
215 return std::nullopt;
216 }
217}
218
219QSqlDatabase &AsyncSqlDatabase::db()
220{
221 return d->database;
222}
223
224void printSqlError(const QSqlQuery &query)
225{
226 qCWarning(asyncdatabase) << "SQL error:" << query.lastError().text();
227}
228
229std::optional<QSqlQuery> AsyncSqlDatabase::prepareQuery(const QSqlDatabase &database, const QString &sqlQuery)
230{
231 qCDebug(asyncdatabase) << "Running" << sqlQuery;
232
233 // Check whether we already have a prepared version of this query
234 if (d->preparedQueryCache.contains(sqlQuery)) {
235 return d->preparedQueryCache[sqlQuery];
236 }
237
238 // If not, prepare one
239 QSqlQuery query(database);
240
241 // If this fails, return without caching the query
242 if (!query.prepare(sqlQuery)) {
243 printSqlError(query);
244 return {};
245 }
246
247 // Else, cache the prepared query
248 d->preparedQueryCache.insert({sqlQuery, query});
249 return query;
250}
251
252QSqlQuery AsyncSqlDatabase::runQuery(QSqlQuery &&query)
253{
254 if (!query.exec()) {
255 printSqlError(query);
256 }
257 return std::move(query);
258}
259
260}
261
262struct DatabaseConfigurationPrivate : public QSharedData {
264 std::optional<QString> hostName;
265 std::optional<QString> databaseName;
266 std::optional<QString> userName;
267 std::optional<QString> password;
268};
269
270DatabaseConfiguration::DatabaseConfiguration() : d(new DatabaseConfigurationPrivate)
271{}
272
273DatabaseConfiguration::~DatabaseConfiguration() = default;
274DatabaseConfiguration::DatabaseConfiguration(const DatabaseConfiguration &) = default;
275
277 d->type = type;
278}
279
280void DatabaseConfiguration::setType(DatabaseType type)
281{
282 switch (type) {
283 case DatabaseType::SQLite:
284 d->type = QStringLiteral("QSQLITE");
285 return;
286 }
287
288 Q_UNREACHABLE();
289}
290
292 return d->type;
293}
294
296 d->hostName = hostName;
297}
298
299const std::optional<QString> &DatabaseConfiguration::hostName() const {
300 return d->hostName;
301}
302
304 d->databaseName = databaseName;
305}
306
307const std::optional<QString> &DatabaseConfiguration::databaseName() const {
308 return d->databaseName;
309}
310
312 d->userName = userName;
313}
314
315const std::optional<QString> &DatabaseConfiguration::userName() const {
316 return d->userName;
317}
318
320 d->password = password;
321}
322
323const std::optional<QString> &DatabaseConfiguration::password() const {
324 return d->password;
325}
326
327
328struct ThreadedDatabasePrivate {
329 asyncdatabase_private::AsyncSqlDatabase db;
330};
331
332std::unique_ptr<ThreadedDatabase> ThreadedDatabase::establishConnection(const DatabaseConfiguration &config) {
333 auto threadedDb = std::unique_ptr<ThreadedDatabase>(new ThreadedDatabase());
334 threadedDb->setObjectName(QStringLiteral("database thread"));
335 threadedDb->d->db.moveToThread(&*threadedDb);
336 threadedDb->start();
337 threadedDb->d->db.establishConnection(config);
338 return threadedDb;
339}
340
341auto ThreadedDatabase::runMigrations(const QString &migrationDirectory) -> QFuture<void> {
342 return d->db.runMigrations(migrationDirectory);
343}
344
346 return d->db.setCurrentMigrationLevel(migrationName);
347}
348
349ThreadedDatabase::ThreadedDatabase()
350 : QThread()
351 , d(std::make_unique<ThreadedDatabasePrivate>())
352{
353}
354
355ThreadedDatabase::~ThreadedDatabase()
356{
357 quit();
358 wait();
359}
360
361asyncdatabase_private::AsyncSqlDatabase &ThreadedDatabase::db()
362{
363 return d->db;
364}
Options for connecting to a database.
void setDatabaseName(const QString &databaseName)
Set the name of the database (path of the file for SQLite)
void setType(const QString &type)
Set the name of the database driver. If it is included in DatabaseType, use the enum overload instead...
void setHostName(const QString &hostName)
Set the hostname.
void setUserName(const QString &userName)
Set user name.
const QString & type() const
Get the name of the database driver.
void setPassword(const QString &password)
Set password.
A database connection that lives on a new thread.
auto runMigrations(const QString &migrationDirectory) -> QFuture< void >
Run the database migrations in the given directory.
static std::unique_ptr< ThreadedDatabase > establishConnection(const DatabaseConfiguration &config)
Connect to a database.
auto setCurrentMigrationLevel(const QString &migrationName) -> QFuture< void >
Declare that the database is currently at the state of the migration in the migration subdirectory mi...
Type type(const QSqlDatabase &db)
std::optional< QSqlQuery > query(const QString &queryStatement)
KIOCORE_EXPORT QString dir(const QString &fileClass)
QString name(StandardAction id)
QChar separator()
QSqlDatabase addDatabase(QSqlDriver *driver, const QString &connectionName)
QSqlDatabase database(const QString &connectionName, bool open)
bool rollback()
bool transaction()
QString fromUtf8(QByteArrayView str)
void quit()
bool wait(QDeadlineTimer deadline)
bool isValid() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:01:03 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.