File indexing completed on 2024-12-08 12:44:36

0001 // SPDX-FileCopyrightText: 2022 Jonah BrĂ¼chert <jbb@kaidan.im>
0002 //
0003 // SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0004 
0005 #include "threadeddatabase.h"
0006 #include "threadeddatabase_p.h"
0007 
0008 #include <QDir>
0009 #include <QSqlDatabase>
0010 #include <QSqlQuery>
0011 #include <QUrl>
0012 #include <QStringBuilder>
0013 #include <QVariant>
0014 #include <QSqlResult>
0015 #include <QSqlError>
0016 #include <QLoggingCategory>
0017 
0018 #include <unordered_map>
0019 
0020 #define SCHAMA_MIGRATIONS_TABLE "__qt_schema_migrations"
0021 
0022 Q_DECLARE_LOGGING_CATEGORY(asyncdatabase)
0023 Q_LOGGING_CATEGORY(asyncdatabase, "futuresql")
0024 
0025 namespace asyncdatabase_private {
0026 
0027 // migrations
0028 void createInternalTable(QSqlDatabase &database) {
0029     QSqlQuery query(QStringLiteral("create table if not exists " SCHAMA_MIGRATIONS_TABLE " ("
0030                                         "version Text primary key not null, "
0031                                         "run_on timestamp not null default current_timestamp)"), database);
0032     if (!query.exec()) {
0033         printSqlError(query);
0034     }
0035 }
0036 
0037 void markMigrationRun(QSqlDatabase &database, const QString &name) {
0038     qCDebug(asyncdatabase) << "Marking migration" << name << "as done.";
0039 
0040     QSqlQuery query(database);
0041     if (!query.prepare(QStringLiteral("insert into " SCHAMA_MIGRATIONS_TABLE " (version) values (:name)"))) {
0042         printSqlError(query);
0043     }
0044     query.bindValue(QStringLiteral(":name"), name);
0045     if (!query.exec()) {
0046         printSqlError(query);
0047     }
0048 }
0049 
0050 QString currentDatabaseVersion(QSqlDatabase &database) {
0051     QSqlQuery query(database);
0052     query.prepare(QStringLiteral("select version from " SCHAMA_MIGRATIONS_TABLE " order by version desc limit 1"));
0053     query.exec();
0054 
0055     if (query.next()) {
0056         return query.value(0).toString();
0057     } else {
0058         return {};
0059     }
0060 }
0061 
0062 void runDatabaseMigrations(QSqlDatabase &database, const QString &migrationDirectory)
0063 {
0064     createInternalTable(database);
0065 
0066     QDir dir(migrationDirectory);
0067     const auto entries = dir.entryList(QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot, QDir::SortFlag::Name);
0068 
0069     const QString currentVersion = currentDatabaseVersion(database);
0070     for (const auto &entry : entries) {
0071         QDir subdir(entry);
0072         if (subdir.dirName() > currentVersion) {
0073             QFile file(migrationDirectory % QDir::separator() % entry % QDir::separator() % u"up.sql");
0074             if (!file.open(QFile::ReadOnly)) {
0075                 qCDebug(asyncdatabase) << "Failed to open migration file" << file.fileName();
0076             }
0077             qCDebug(asyncdatabase) << "Running migration" << subdir.dirName();
0078 
0079             database.transaction();
0080 
0081             // Hackish
0082             const auto statements = file.readAll().split(';');
0083 
0084             bool migrationSuccessful = true;
0085             for (const QByteArray &statement : statements) {
0086                 const auto trimmedStatement = QString::fromUtf8(statement.trimmed());
0087                 QSqlQuery query(database);
0088 
0089                 if (!trimmedStatement.isEmpty()) {
0090                     qCDebug(asyncdatabase) << "Running" << trimmedStatement;
0091                     if (!query.prepare(trimmedStatement)) {
0092                         printSqlError(query);
0093                         migrationSuccessful = false;
0094                     } else {
0095                         bool success = query.exec();
0096                         migrationSuccessful &= success;
0097                         if (!success) {
0098                             printSqlError(query);
0099                         }
0100                     }
0101                 }
0102             }
0103 
0104             if (migrationSuccessful) {
0105                 database.commit();
0106                 markMigrationRun(database, subdir.dirName());
0107             } else {
0108                 qCWarning(asyncdatabase) << "Migration" << subdir.dirName() << "failed, retrying next time.";
0109                 qCWarning(asyncdatabase) << "Stopping migrations here, as the next migration may depens on this one.";
0110                 database.rollback();
0111                 return;
0112             }
0113         }
0114     }
0115     qCDebug(asyncdatabase) << "Migrations finished";
0116 }
0117 
0118 struct AsyncSqlDatabasePrivate {
0119     QSqlDatabase database;
0120     std::unordered_map<QString, QSqlQuery> preparedQueryCache;
0121 };
0122 
0123 // Internal asynchronous database class
0124 QFuture<void> AsyncSqlDatabase::establishConnection(const DatabaseConfiguration &configuration)
0125 {
0126     return runAsync([=, this] {
0127         d->database = QSqlDatabase::addDatabase(configuration.type());
0128         if (configuration.databaseName()) {
0129             d->database.setDatabaseName(*configuration.databaseName());
0130         }
0131         if (configuration.hostName()) {
0132             d->database.setHostName(*configuration.hostName());
0133         }
0134         if (configuration.userName()) {
0135             d->database.setUserName(*configuration.userName());
0136         }
0137         if (configuration.password()) {
0138             d->database.setPassword(*configuration.password());
0139         }
0140 
0141         if (!d->database.open()) {
0142             qCDebug(asyncdatabase) << "Failed to open database" << d->database.lastError().text();
0143             if (configuration.databaseName()) {
0144                 qCDebug(asyncdatabase) << "Tried to use database" << *configuration.databaseName();
0145             }
0146         }
0147     });
0148 }
0149 
0150 auto AsyncSqlDatabase::runMigrations(const QString &migrationDirectory) -> QFuture<void> {
0151     return runAsync([=, this] {
0152         runDatabaseMigrations(d->database, migrationDirectory);
0153     });
0154 }
0155 auto AsyncSqlDatabase::setCurrentMigrationLevel(const QString &migrationName) -> QFuture<void> {
0156     return runAsync([=, this] {
0157         createInternalTable(d->database);
0158         markMigrationRun(d->database, migrationName);
0159     });
0160 }
0161 
0162 AsyncSqlDatabase::AsyncSqlDatabase()
0163     : QObject()
0164     , d(std::make_unique<AsyncSqlDatabasePrivate>())
0165 {
0166 }
0167 
0168 AsyncSqlDatabase::~AsyncSqlDatabase() {
0169     runAsync([db = d->database] {
0170         QSqlDatabase::removeDatabase(db.databaseName());
0171     });
0172 };
0173 
0174 Row AsyncSqlDatabase::retrieveRow(const QSqlQuery &query) {
0175     Row row;
0176     int i = 0;
0177 
0178     while (true) {
0179         if (query.isValid()) {
0180             QVariant value = query.value(i);
0181             if (value.isValid()) {
0182                 row.push_back(std::move(value));
0183                 i++;
0184             } else {
0185                 break;
0186             }
0187         } else {
0188             break;
0189         }
0190     }
0191     return row;
0192 }
0193 
0194 Rows AsyncSqlDatabase::retrieveRows(QSqlQuery &query)
0195 {
0196     Rows rows;
0197     while (query.next()) {
0198         rows.push_back(retrieveRow(query));
0199     }
0200 
0201     return rows;
0202 }
0203 
0204 std::optional<Row> AsyncSqlDatabase::retrieveOptionalRow(QSqlQuery &query)
0205 {
0206     query.next();
0207 
0208     if (query.isValid()) {
0209         return retrieveRow(query);
0210     } else {
0211         return std::nullopt;
0212     }
0213 }
0214 
0215 QSqlDatabase &AsyncSqlDatabase::db()
0216 {
0217     return d->database;
0218 }
0219 
0220 void printSqlError(const QSqlQuery &query)
0221 {
0222     qCDebug(asyncdatabase) << "SQL error:" << query.lastError().text();
0223 }
0224 
0225 std::optional<QSqlQuery> AsyncSqlDatabase::prepareQuery(const QSqlDatabase &database, const QString &sqlQuery)
0226 {
0227     qCDebug(asyncdatabase) << "Running" << sqlQuery;
0228 
0229     // Check whether we already have a prepared version of this query
0230     if (d->preparedQueryCache.contains(sqlQuery)) {
0231         return d->preparedQueryCache[sqlQuery];
0232     }
0233 
0234     // If not, prepare one
0235     QSqlQuery query(database);
0236 
0237     // If this fails, return without caching the query
0238     if (!query.prepare(sqlQuery)) {
0239         printSqlError(query);
0240         return {};
0241     }
0242 
0243     // Else, cache the prepared query
0244     d->preparedQueryCache.insert({sqlQuery, query});
0245     return query;
0246 }
0247 
0248 QSqlQuery AsyncSqlDatabase::runQuery(QSqlQuery &&query)
0249 {
0250     if (!query.exec()) {
0251         printSqlError(query);
0252     }
0253     return std::move(query);
0254 }
0255 
0256 }
0257 
0258 struct DatabaseConfigurationPrivate : public QSharedData {
0259     QString type;
0260     std::optional<QString> hostName;
0261     std::optional<QString> databaseName;
0262     std::optional<QString> userName;
0263     std::optional<QString> password;
0264 };
0265 
0266 DatabaseConfiguration::DatabaseConfiguration() : d(new DatabaseConfigurationPrivate)
0267 {}
0268 
0269 DatabaseConfiguration::~DatabaseConfiguration() = default;
0270 DatabaseConfiguration::DatabaseConfiguration(const DatabaseConfiguration &) = default;
0271 
0272 void DatabaseConfiguration::setType(const QString &type) {
0273     d->type = type;
0274 }
0275 
0276 void DatabaseConfiguration::setType(DatabaseType type)
0277 {
0278     switch (type) {
0279     case DatabaseType::SQLite:
0280         d->type = QStringLiteral("QSQLITE");
0281         return;
0282     }
0283 
0284     Q_UNREACHABLE();
0285 }
0286 
0287 const QString &DatabaseConfiguration::type() const {
0288     return d->type;
0289 }
0290 
0291 void DatabaseConfiguration::setHostName(const QString &hostName) {
0292     d->hostName = hostName;
0293 }
0294 
0295 const std::optional<QString> &DatabaseConfiguration::hostName() const {
0296     return d->hostName;
0297 }
0298 
0299 void DatabaseConfiguration::setDatabaseName(const QString &databaseName) {
0300     d->databaseName = databaseName;
0301 }
0302 
0303 const std::optional<QString> &DatabaseConfiguration::databaseName() const {
0304     return d->databaseName;
0305 }
0306 
0307 void DatabaseConfiguration::setUserName(const QString &userName) {
0308     d->userName = userName;
0309 }
0310 
0311 const std::optional<QString> &DatabaseConfiguration::userName() const {
0312     return d->userName;
0313 }
0314 
0315 void DatabaseConfiguration::setPassword(const QString &password) {
0316     d->password = password;
0317 }
0318 
0319 const std::optional<QString> &DatabaseConfiguration::password() const {
0320     return d->password;
0321 }
0322 
0323 
0324 struct ThreadedDatabasePrivate {
0325     asyncdatabase_private::AsyncSqlDatabase db;
0326 };
0327 
0328 std::unique_ptr<ThreadedDatabase> ThreadedDatabase::establishConnection(const DatabaseConfiguration &config) {
0329     auto threadedDb = std::unique_ptr<ThreadedDatabase>(new ThreadedDatabase());
0330     threadedDb->setObjectName(QStringLiteral("database thread"));
0331     threadedDb->d->db.moveToThread(&*threadedDb);
0332     threadedDb->start();
0333     threadedDb->d->db.establishConnection(config);
0334     return threadedDb;
0335 }
0336 
0337 auto ThreadedDatabase::runMigrations(const QString &migrationDirectory) -> QFuture<void> {
0338     return d->db.runMigrations(migrationDirectory);
0339 }
0340 
0341 auto ThreadedDatabase::setCurrentMigrationLevel(const QString &migrationName) -> QFuture<void> {
0342     return d->db.setCurrentMigrationLevel(migrationName);
0343 }
0344 
0345 ThreadedDatabase::ThreadedDatabase()
0346     : QThread()
0347     , d(std::make_unique<ThreadedDatabasePrivate>())
0348 {
0349 }
0350 
0351 ThreadedDatabase::~ThreadedDatabase()
0352 {
0353     quit();
0354     wait();
0355 }
0356 
0357 asyncdatabase_private::AsyncSqlDatabase &ThreadedDatabase::db()
0358 {
0359     return d->db;
0360 }