/* * Copyright (C) 2019 Emeric Poupon * * This file is part of LMS. * * LMS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LMS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LMS. If not, see . */ #include #include #include #include #include #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Service.hpp" #include "database/Session.hpp" #include "database/objects/User.hpp" #include "Db.hpp" namespace lms::db { namespace { class Connection : public Wt::Dbo::backend::Sqlite3 { public: Connection(const std::filesystem::path& dbPath) : Wt::Dbo::backend::Sqlite3{ dbPath.string() } , _dbPath{ dbPath } { prepare(); } Connection(const Connection& other) : Wt::Dbo::backend::Sqlite3{ other } , _dbPath{ other._dbPath } { prepare(); } ~Connection() override = default; private: Connection& operator=(const Connection&) = delete; Connection(Connection&&) = delete; Connection&& operator=(Connection&&) = delete; std::unique_ptr clone() const override { return std::make_unique(*this); } void prepare() { LMS_LOG(DB, DEBUG, "Setting per-connection settings..."); executeSql("PRAGMA journal_mode=WAL"); executeSql("PRAGMA synchronous=normal"); LMS_LOG(DB, DEBUG, "Setting per-connection settings done!"); } std::filesystem::path _dbPath; }; enum class IntegrityCheckType { Quick, Full }; bool checkDbIntegrity(Wt::Dbo::SqlConnection& connection, IntegrityCheckType checkType, std::function errorCallback) { bool integrityCheckPassed{}; auto statement = connection.prepareStatement(checkType == IntegrityCheckType::Full ? "PRAGMA integrity_check" : "PRAGMA quick_check"); statement->execute(); std::string result; result.reserve(32); while (statement->nextRow()) { result.clear(); statement->getResult(0, &result, result.capacity()); if (result == "ok") { integrityCheckPassed = true; break; } errorCallback(result); } return integrityCheckPassed; } bool checkDbForeignKeyConstraints(Wt::Dbo::SqlConnection& connection, std::function errorCallback) { bool foreignKeyConstraintsPassed{ true }; auto statement = connection.prepareStatement("PRAGMA foreign_key_check"); statement->execute(); std::string table; std::string foreignTable; // see https://www.sqlite.org/pragma.html#pragma_foreign_key_check for expected result while (statement->nextRow()) { foreignKeyConstraintsPassed = false; table.clear(); foreignTable.clear(); long long rowId{}; statement->getResult(0, &table, static_cast(table.capacity())); statement->getResult(1, &rowId); statement->getResult(2, &foreignTable, static_cast(foreignTable.capacity())); errorCallback(table, rowId, foreignTable); } return foreignKeyConstraintsPassed; } std::optional getPageSize(Wt::Dbo::SqlConnection& connection) { auto statement = connection.prepareStatement("PRAGMA page_size"); statement->execute(); std::optional res; while (statement->nextRow()) { assert(!res); int value{}; if (statement->getResult(0, &value)) res = value; break; } return res; } std::optional getCacheSize(Wt::Dbo::SqlConnection& connection) { auto statement = connection.prepareStatement("PRAGMA cache_size"); statement->execute(); std::optional res; while (statement->nextRow()) { assert(!res); int value{}; if (statement->getResult(0, &value)) res = value; break; } return res; } void getCompileOptions(Wt::Dbo::SqlConnection& connection, std::function callback) { auto statement = connection.prepareStatement("PRAGMA compile_options"); statement->execute(); std::string res; while (statement->nextRow()) { res.clear(); if (statement->getResult(0, &res, static_cast(res.capacity()))) callback(res); } } } // namespace std::unique_ptr createDb(const std::filesystem::path& dbPath, std::size_t connectionCount) { return std::make_unique(dbPath, connectionCount); } // Session living class handling the database and the login Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount) { Wt::Dbo::logToWt(); std::string checkType{ "quick" }; LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath); auto connection{ std::make_unique(dbPath) }; if (core::IConfig * config{ core::Service::get() }) // may not be here on testU { connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false"); checkType = config->getString("db-integrity-check", "quick"); } auto connectionPool{ std::make_unique(std::move(connection), connectionCount) }; connectionPool->setTimeout(std::chrono::seconds{ 10 }); _connectionPool = std::move(connectionPool); executeSql("PRAGMA temp_store=MEMORY"); executeSql("PRAGMA cache_size=-8000"); executeSql("PRAGMA automatic_index=0"); logPageSize(); logCacheSize(); logCompileOptions(); if (checkType == "quick") { performQuickCheck(); } else if (checkType == "full") { performIntegrityCheck(); performForeignKeyConstraintsCheck(); } else if (checkType != "none") { throw Exception("Invalid 'db-integrity-check' value: '" + checkType + "'. Expected 'quick', 'full' or 'none'."); } } void Db::executeSql(const std::string& sql) { ScopedConnection connection{ *_connectionPool }; connection->executeSql(sql); } Session& Db::getTLSSession() { static thread_local Session* tlsSession{}; if (!tlsSession) { auto newSession{ std::make_unique(*this) }; tlsSession = newSession.get(); { std::scoped_lock lock{ _tlsSessionsMutex }; _tlsSessions.push_back(std::move(newSession)); } } // For now, multiple databases are not handled assert(&tlsSession->getDb() == this); return *tlsSession; } void Db::logPageSize() { ScopedConnection connection{ *_connectionPool }; const std::optional pageSize{ getPageSize(*connection) }; if (pageSize) LMS_LOG(DB, INFO, "Page size set to " << *pageSize); } void Db::logCacheSize() { ScopedConnection connection{ *_connectionPool }; const std::optional cacheSize{ getCacheSize(*connection) }; if (cacheSize) LMS_LOG(DB, INFO, "Cache size set to " << *cacheSize); } void Db::logCompileOptions() { ScopedConnection connection{ *_connectionPool }; LMS_LOG(DB, INFO, "Sqlite3 compile options:"); getCompileOptions(*connection, [](std::string_view compileOption) { LMS_LOG(DB, INFO, compileOption); }); } void Db::performQuickCheck() { ScopedConnection connection{ *_connectionPool }; LMS_LOG(DB, INFO, "Performing quick database check..."); // Quick check is just a simple integrity check const bool quickCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Quick, [&](std::string_view error) { LMS_LOG(DB, ERROR, "Quick check error: " << error); }) }; if (quickCheckPassed) LMS_LOG(DB, INFO, "Quick database check passed!"); else LMS_LOG(DB, ERROR, "Quick database check done with errors!"); } void Db::performIntegrityCheck() { ScopedConnection connection{ *_connectionPool }; LMS_LOG(DB, INFO, "Checking database integrity..."); const bool integrityCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Full, [&](std::string_view error) { LMS_LOG(DB, ERROR, "Integrity check error: " << error); }) }; if (integrityCheckPassed) LMS_LOG(DB, INFO, "Database integrity check passed!"); else LMS_LOG(DB, ERROR, "Database integrity check done with errors!"); } void Db::performForeignKeyConstraintsCheck() { ScopedConnection connection{ *_connectionPool }; LMS_LOG(DB, INFO, "Checking foreign key constraints..."); const bool foreignKeyConstraintsPassed{ checkDbForeignKeyConstraints(*connection, [&](std::string_view table, long long rowId, std::string_view referredTable) { LMS_LOG(DB, ERROR, "Foreign key constraint failed in table '" << table << "', rowid = " << rowId << ", referred table = '" << referredTable << "'"); }) }; if (!foreignKeyConstraintsPassed) throw Exception("Foreign key constraints check failed! Please restore from a backup or recreate the database."); LMS_LOG(DB, INFO, "Foreign key constraints check passed!"); } Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool) : _connectionPool{ pool } , _connection{ _connectionPool.getConnection() } { } Db::ScopedConnection::~ScopedConnection() { _connectionPool.returnConnection(std::move(_connection)); } Wt::Dbo::SqlConnection* Db::ScopedConnection::operator->() const { return _connection.get(); } } // namespace lms::db