diff --git a/src/database/database-postgresql.cpp b/src/database/database-postgresql.cpp index a84c89fa8..85beff995 100644 --- a/src/database/database-postgresql.cpp +++ b/src/database/database-postgresql.cpp @@ -38,6 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "settings.h" #include "remoteplayer.h" #include "server/player_sao.h" +#include Database_PostgreSQL::Database_PostgreSQL(const std::string &connect_string, const char *type) : @@ -812,5 +813,211 @@ void AuthDatabasePostgreSQL::writePrivileges(const AuthEntry &authEntry) } } +ModMetadataDatabasePostgreSQL::ModMetadataDatabasePostgreSQL(const std::string &connect_string): + Database_PostgreSQL(connect_string, "_mod_storage"), + ModMetadataDatabase() +{ + connectToDatabase(); +} + +void ModMetadataDatabasePostgreSQL::createDatabase() +{ + createTableIfNotExists("mod_storage", + "CREATE TABLE mod_storage (" + "modname TEXT NOT NULL," + "key BYTEA NOT NULL," + "value BYTEA NOT NULL," + "PRIMARY KEY (modname, key)" + ");"); + + infostream << "PostgreSQL: Mod Storage Database was initialized." << std::endl; +} + +void ModMetadataDatabasePostgreSQL::initStatements() +{ + prepareStatement("get_all", + "SELECT key, value FROM mod_storage WHERE modname = $1"); + prepareStatement("get_all_keys", + "SELECT key FROM mod_storage WHERE modname = $1"); + prepareStatement("get", + "SELECT value FROM mod_storage WHERE modname = $1 AND key = $2::bytea"); + prepareStatement("has", + "SELECT true FROM mod_storage WHERE modname = $1 AND key = $2::bytea"); + if (getPGVersion() < 90500) { + prepareStatement("set_insert", + "INSERT INTO mod_storage (modname, key, value) " + "SELECT $1, $2::bytea, $3::bytea " + "WHERE NOT EXISTS (" + "SELECT true FROM mod_storage WHERE modname = $1 AND key = $2::bytea" + ")"); + prepareStatement("set_update", + "UPDATE mod_storage SET value = $3::bytea WHERE modname = $1 AND key = $2::bytea"); + } else { + prepareStatement("set", + "INSERT INTO mod_storage (modname, key, value) VALUES ($1, $2::bytea, $3::bytea) " + "ON CONFLICT ON CONSTRAINT mod_storage_pkey DO " + "UPDATE SET value = $3::bytea"); + } + prepareStatement("remove", + "DELETE FROM mod_storage WHERE modname = $1 AND key = $2::bytea"); + prepareStatement("remove_all", + "DELETE FROM mod_storage WHERE modname = $1"); + prepareStatement("list", + "SELECT DISTINCT modname FROM mod_storage"); +} + +bool ModMetadataDatabasePostgreSQL::getModEntries(const std::string &modname, StringMap *storage) +{ + verifyDatabase(); + + const void *args[] = { modname.c_str() }; + const int argLen[] = { -1 }; + const int argFmt[] = { 0 }; + PGresult *results = execPrepared("get_all", ARRLEN(args), + args, argLen, argFmt, false); + + int numrows = PQntuples(results); + + for (int row = 0; row < numrows; ++row) { + std::string key(PQgetvalue(results, row, 0), PQgetlength(results, row, 0)); + std::string value(PQgetvalue(results, row, 1), PQgetlength(results, row, 1)); + storage->emplace(std::move(key), std::move(value)); + } + + PQclear(results); + + return true; +} + +bool ModMetadataDatabasePostgreSQL::getModKeys(const std::string &modname, + std::vector *storage) +{ + verifyDatabase(); + + const void *args[] = { modname.c_str() }; + const int argLen[] = { -1 }; + const int argFmt[] = { 0 }; + PGresult *results = execPrepared("get_all_keys", ARRLEN(args), + args, argLen, argFmt, false); + + int numrows = PQntuples(results); + + storage->reserve(storage->size() + numrows); + for (int row = 0; row < numrows; ++row) + storage->emplace_back(PQgetvalue(results, row, 0), PQgetlength(results, row, 0)); + + PQclear(results); + + return true; +} + +bool ModMetadataDatabasePostgreSQL::getModEntry(const std::string &modname, + const std::string &key, std::string *value) +{ + verifyDatabase(); + + const void *args[] = { modname.c_str(), key.c_str() }; + const int argLen[] = { -1, (int)MYMIN(key.size(), INT_MAX) }; + const int argFmt[] = { 0, 1 }; + PGresult *results = execPrepared("get", ARRLEN(args), args, argLen, argFmt, false); + + int numrows = PQntuples(results); + bool found = numrows > 0; + + if (found) + value->assign(PQgetvalue(results, 0, 0), PQgetlength(results, 0, 0)); + + PQclear(results); + + return found; +} + +bool ModMetadataDatabasePostgreSQL::hasModEntry(const std::string &modname, + const std::string &key) +{ + verifyDatabase(); + + const void *args[] = { modname.c_str(), key.c_str() }; + const int argLen[] = { -1, (int)MYMIN(key.size(), INT_MAX) }; + const int argFmt[] = { 0, 1 }; + PGresult *results = execPrepared("has", ARRLEN(args), args, argLen, argFmt, false); + + int numrows = PQntuples(results); + bool found = numrows > 0; + + PQclear(results); + + return found; +} + +bool ModMetadataDatabasePostgreSQL::setModEntry(const std::string &modname, + const std::string &key, const std::string &value) +{ + verifyDatabase(); + + const void *args[] = { modname.c_str(), key.c_str(), value.c_str() }; + const int argLen[] = { + -1, + (int)MYMIN(key.size(), INT_MAX), + (int)MYMIN(value.size(), INT_MAX), + }; + const int argFmt[] = { 0, 1, 1 }; + if (getPGVersion() < 90500) { + execPrepared("set_insert", ARRLEN(args), args, argLen, argFmt); + execPrepared("set_update", ARRLEN(args), args, argLen, argFmt); + } else { + execPrepared("set", ARRLEN(args), args, argLen, argFmt); + } + + return true; +} + +bool ModMetadataDatabasePostgreSQL::removeModEntry(const std::string &modname, + const std::string &key) +{ + verifyDatabase(); + + const void *args[] = { modname.c_str(), key.c_str() }; + const int argLen[] = { -1, (int)MYMIN(key.size(), INT_MAX) }; + const int argFmt[] = { 0, 1 }; + PGresult *results = execPrepared("remove", ARRLEN(args), args, argLen, argFmt, false); + + int affected = atoi(PQcmdTuples(results)); + + PQclear(results); + + return affected > 0; +} + +bool ModMetadataDatabasePostgreSQL::removeModEntries(const std::string &modname) +{ + verifyDatabase(); + + const void *args[] = { modname.c_str() }; + const int argLen[] = { -1 }; + const int argFmt[] = { 0 }; + PGresult *results = execPrepared("remove_all", ARRLEN(args), args, argLen, argFmt, false); + + int affected = atoi(PQcmdTuples(results)); + + PQclear(results); + + return affected > 0; +} + +void ModMetadataDatabasePostgreSQL::listMods(std::vector *res) +{ + verifyDatabase(); + + PGresult *results = execPrepared("list", 0, NULL, false); + + int numrows = PQntuples(results); + + for (int row = 0; row < numrows; ++row) + res->emplace_back(PQgetvalue(results, row, 0), PQgetlength(results, row, 0)); + + PQclear(results); +} + #endif // USE_POSTGRESQL diff --git a/src/database/database-postgresql.h b/src/database/database-postgresql.h index 0a9ead01e..22269d362 100644 --- a/src/database/database-postgresql.h +++ b/src/database/database-postgresql.h @@ -169,3 +169,27 @@ protected: private: virtual void writePrivileges(const AuthEntry &authEntry); }; + +class ModMetadataDatabasePostgreSQL : private Database_PostgreSQL, public ModMetadataDatabase +{ +public: + ModMetadataDatabasePostgreSQL(const std::string &connect_string); + ~ModMetadataDatabasePostgreSQL() = default; + + bool getModEntries(const std::string &modname, StringMap *storage); + bool getModKeys(const std::string &modname, std::vector *storage); + bool getModEntry(const std::string &modname, const std::string &key, std::string *value); + bool hasModEntry(const std::string &modname, const std::string &key); + bool setModEntry(const std::string &modname, + const std::string &key, const std::string &value); + bool removeModEntry(const std::string &modname, const std::string &key); + bool removeModEntries(const std::string &modname); + void listMods(std::vector *res); + + void beginSave() { Database_PostgreSQL::beginSave(); } + void endSave() { Database_PostgreSQL::endSave(); } + +protected: + virtual void createDatabase(); + virtual void initStatements(); +}; diff --git a/src/server.cpp b/src/server.cpp index 762450a75..416e51f30 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -67,6 +67,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "server/serverinventorymgr.h" #include "translation.h" #include "database/database-sqlite3.h" +#if USE_POSTGRESQL +#include "database/database-postgresql.h" +#endif #include "database/database-files.h" #include "database/database-dummy.h" #include "gameparams.h" @@ -4025,6 +4028,14 @@ ModMetadataDatabase *Server::openModStorageDatabase(const std::string &backend, if (backend == "sqlite3") return new ModMetadataDatabaseSQLite3(world_path); +#if USE_POSTGRESQL + if (backend == "postgresql") { + std::string connect_string; + world_mt.getNoEx("pgsql_mod_storage_connection", connect_string); + return new ModMetadataDatabasePostgreSQL(connect_string); + } +#endif // USE_POSTGRESQL + if (backend == "files") return new ModMetadataDatabaseFiles(world_path); diff --git a/src/unittest/test_modmetadatadatabase.cpp b/src/unittest/test_modmetadatadatabase.cpp index b93ff7e3b..b46feb884 100644 --- a/src/unittest/test_modmetadatadatabase.cpp +++ b/src/unittest/test_modmetadatadatabase.cpp @@ -20,12 +20,18 @@ with this program; if not, write to the Free Software Foundation, Inc., // This file is an edited copy of test_authdatabase.cpp +#include "cmake_config.h" + #include "test.h" #include +#include #include "database/database-dummy.h" #include "database/database-files.h" #include "database/database-sqlite3.h" +#if USE_POSTGRESQL +#include "database/database-postgresql.h" +#endif #include "filesys.h" namespace @@ -104,6 +110,46 @@ private: std::string dir; ModMetadataDatabase *mod_meta_db = nullptr; }; + +#if USE_POSTGRESQL +void clearPostgreSQLDatabase(const std::string &connect_string) +{ + ModMetadataDatabasePostgreSQL db(connect_string); + std::vector modnames; + db.beginSave(); + db.listMods(&modnames); + for (const std::string &modname : modnames) + db.removeModEntries(modname); + db.endSave(); +} + +class PostgreSQLProvider : public ModMetadataDatabaseProvider +{ +public: + PostgreSQLProvider(const std::string &connect_string): m_connect_string(connect_string) {} + + ~PostgreSQLProvider() + { + if (m_db) + m_db->endSave(); + delete m_db; + } + + ModMetadataDatabase *getModMetadataDatabase() override + { + if (m_db) + m_db->endSave(); + delete m_db; + m_db = new ModMetadataDatabasePostgreSQL(m_connect_string); + m_db->beginSave(); + return m_db; + }; + +private: + std::string m_connect_string; + ModMetadataDatabase *m_db = nullptr; +}; +#endif // USE_POSTGRESQL } class TestModMetadataDatabase : public TestBase @@ -193,6 +239,33 @@ void TestModMetadataDatabase::runTests(IGameDef *gamedef) runTestsForCurrentDB(); delete mod_meta_provider; + +#if USE_POSTGRESQL + const char *env_postgresql_connect_string = getenv("MINETEST_POSTGRESQL_CONNECT_STRING"); + if (env_postgresql_connect_string) { + std::string connect_string(env_postgresql_connect_string); + + rawstream << "-------- PostgreSQL database (same object)" << std::endl; + + clearPostgreSQLDatabase(connect_string); + mod_meta_db = new ModMetadataDatabasePostgreSQL(connect_string); + mod_meta_provider = new FixedProvider(mod_meta_db); + + runTestsForCurrentDB(); + + delete mod_meta_db; + delete mod_meta_provider; + + rawstream << "-------- PostgreSQL database (new objects)" << std::endl; + + clearPostgreSQLDatabase(connect_string); + mod_meta_provider = new PostgreSQLProvider(connect_string); + + runTestsForCurrentDB(); + + delete mod_meta_provider; + } +#endif // USE_POSTGRESQL } ////////////////////////////////////////////////////////////////////////////////