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 }