diff --git a/src/fb-cpp/BackupManager.cpp b/src/fb-cpp/BackupManager.cpp index 9aa38d9..c6fb001 100644 --- a/src/fb-cpp/BackupManager.cpp +++ b/src/fb-cpp/BackupManager.cpp @@ -24,6 +24,7 @@ #include "BackupManager.h" #include "Client.h" +#include using namespace fbcpp; using namespace fbcpp::impl; @@ -84,6 +85,27 @@ void BackupManager::restore(const RestoreOptions& options) if (const auto parallelWorkers = options.getParallelWorkers()) builder->insertInt(&statusWrapper, isc_spb_res_parallel_workers, static_cast(*parallelWorkers)); + if (const auto replicaMode = options.getReplicaMode()) + { + std::uint8_t modeVal = 0; + switch (*replicaMode) + { + case ReplicaMode::NONE: + modeVal = isc_spb_res_rm_none; + break; + case ReplicaMode::READ_ONLY: + modeVal = isc_spb_res_rm_readonly; + break; + case ReplicaMode::READ_WRITE: + modeVal = isc_spb_res_rm_readwrite; + break; + default: + assert(false); + break; + } + builder->insertBytes(&statusWrapper, isc_spb_res_replica_mode, &modeVal, 1u); + } + const auto buffer = builder->getBuffer(&statusWrapper); const auto length = builder->getBufferLength(&statusWrapper); diff --git a/src/fb-cpp/BackupManager.h b/src/fb-cpp/BackupManager.h index 137c3d9..89b3bb1 100644 --- a/src/fb-cpp/BackupManager.h +++ b/src/fb-cpp/BackupManager.h @@ -296,12 +296,30 @@ namespace fbcpp return *this; } + /// + /// Returns the replica mode. + /// + const std::optional& getReplicaMode() const + { + return replicaMode; + } + + /// + /// Sets the replica mode. + /// + RestoreOptions& setReplicaMode(ReplicaMode value) + { + replicaMode = value; + return *this; + } + private: std::vector databaseFiles; std::vector backupFiles; bool replace = false; ServiceManager::VerboseOutput verboseOutput; std::optional parallelWorkers; + std::optional replicaMode; }; /// diff --git a/src/fb-cpp/DatabaseManager.cpp b/src/fb-cpp/DatabaseManager.cpp new file mode 100644 index 0000000..0614295 --- /dev/null +++ b/src/fb-cpp/DatabaseManager.cpp @@ -0,0 +1,67 @@ +/* + * MIT License + * + * Copyright (c) 2026 Popa Adrian Marius + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "DatabaseManager.h" +#include "Client.h" +#include + +using namespace fbcpp; +using namespace fbcpp::impl; + + +void DatabaseManager::execute(const DatabaseManagerOptions& options) +{ + StatusWrapper statusWrapper{getClient()}; + auto builder = + fbUnique(getClient().getUtil()->getXpbBuilder(&statusWrapper, fb::IXpbBuilder::SPB_START, nullptr, 0)); + builder->insertTag(&statusWrapper, isc_action_svc_properties); + builder->insertString(&statusWrapper, isc_spb_dbname, options.getDatabase().c_str()); + + if (const auto replicaMode = options.getReplicaMode()) + { + std::uint8_t modeVal = 0; + switch (*replicaMode) + { + case ReplicaMode::NONE: + modeVal = isc_spb_prp_rm_none; + break; + case ReplicaMode::READ_ONLY: + modeVal = isc_spb_prp_rm_readonly; + break; + case ReplicaMode::READ_WRITE: + modeVal = isc_spb_prp_rm_readwrite; + break; + default: + assert(false); + break; + } + builder->insertBytes(&statusWrapper, isc_spb_prp_replica_mode, &modeVal, 1u); + } + + const auto buffer = builder->getBuffer(&statusWrapper); + const auto length = builder->getBufferLength(&statusWrapper); + + startAction(std::vector(buffer, buffer + length)); + waitForCompletion(); +} diff --git a/src/fb-cpp/DatabaseManager.h b/src/fb-cpp/DatabaseManager.h new file mode 100644 index 0000000..887b377 --- /dev/null +++ b/src/fb-cpp/DatabaseManager.h @@ -0,0 +1,100 @@ +/* + * MIT License + * + * Copyright (c) 2026 Popa Adrian Marius + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef FBCPP_DATABASE_MANAGER_H +#define FBCPP_DATABASE_MANAGER_H + +#include "ServiceManager.h" +#include +#include + + +/// +/// fb-cpp namespace. +/// +namespace fbcpp +{ + /// + /// Represents options used to configure database properties through the service manager. + /// + class DatabaseManagerOptions final + { + public: + /// + /// Returns the database path to be configured. + /// + const std::string& getDatabase() const + { + return database; + } + + /// + /// Sets the database path to be configured. + /// + DatabaseManagerOptions& setDatabase(const std::string& value) + { + database = value; + return *this; + } + + /// + /// Returns the replica mode. + /// + const std::optional& getReplicaMode() const + { + return replicaMode; + } + + /// + /// Sets the replica mode. + /// + DatabaseManagerOptions& setReplicaMode(ReplicaMode value) + { + replicaMode = value; + return *this; + } + + private: + std::string database; + std::optional replicaMode; + }; + + /// + /// Executes configuration and maintenance operations through the Firebird service manager. + /// + class DatabaseManager final : public ServiceManager + { + public: + using ServiceManager::ServiceManager; + + public: + /// + /// Configures database properties using the provided options. + /// + void execute(const DatabaseManagerOptions& options); + }; +} // namespace fbcpp + + +#endif // FBCPP_DATABASE_MANAGER_H diff --git a/src/fb-cpp/ServiceManager.h b/src/fb-cpp/ServiceManager.h index 46d0440..ebe2815 100644 --- a/src/fb-cpp/ServiceManager.h +++ b/src/fb-cpp/ServiceManager.h @@ -42,6 +42,27 @@ /// namespace fbcpp { + /// + /// Replica mode. + /// + enum class ReplicaMode + { + /// + /// The database is not a replica (operates as primary). + /// + NONE, + + /// + /// Read-only replica. + /// + READ_ONLY, + + /// + /// Read-write replica. + /// + READ_WRITE + }; + class Client; /// diff --git a/src/fb-cpp/fb-cpp.h b/src/fb-cpp/fb-cpp.h index a0fa3db..b3f8fc8 100644 --- a/src/fb-cpp/fb-cpp.h +++ b/src/fb-cpp/fb-cpp.h @@ -36,5 +36,6 @@ #include "EventListener.h" #include "ServiceManager.h" #include "BackupManager.h" +#include "DatabaseManager.h" #endif // FBCPP_H diff --git a/src/test/DatabaseManager.cpp b/src/test/DatabaseManager.cpp new file mode 100644 index 0000000..8e86045 --- /dev/null +++ b/src/test/DatabaseManager.cpp @@ -0,0 +1,152 @@ +/* + * MIT License + * + * Copyright (c) 2026 Popa Adrian Marius + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "TestUtil.h" +#include "fb-cpp/BackupManager.h" +#include "fb-cpp/DatabaseManager.h" +#include "fb-cpp/Exception.h" +#include "fb-cpp/Statement.h" +#include "fb-cpp/Transaction.h" +#include + +namespace +{ + ServiceManagerOptions makeServiceManagerOptions() + { + auto options = ServiceManagerOptions{}; + + if (const auto server = getServer()) + options.setServer(server.value()); + + return options; + } +} // namespace + +BOOST_AUTO_TEST_SUITE(DatabaseManagerSuite) + +BOOST_AUTO_TEST_CASE(setReplicaModeNone) +{ + const auto databasePath = getTempFile("DatabaseManager-setReplicaModeNone.fdb", false); + const auto databaseUri = getTempFile("DatabaseManager-setReplicaModeNone.fdb"); + const auto attachmentOptions = AttachmentOptions().setConnectionCharSet("UTF8"); + + { // scope + Attachment attachment{ + CLIENT, databaseUri, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + Transaction transaction{attachment}; + + Statement queryContext{ + attachment, transaction, "select rdb$get_context('SYSTEM', 'REPLICA_MODE') from rdb$database"}; + BOOST_REQUIRE(queryContext.execute(transaction)); + BOOST_CHECK(!queryContext.getString(0).has_value()); + + Statement queryMon{attachment, transaction, "select mon$replica_mode from mon$database"}; + BOOST_REQUIRE(queryMon.execute(transaction)); + BOOST_CHECK_EQUAL(queryMon.getInt32(0).value(), 0); + } + + DatabaseManager manager{CLIENT, makeServiceManagerOptions()}; + manager.execute(DatabaseManagerOptions().setDatabase(databasePath).setReplicaMode(ReplicaMode::READ_ONLY)); + + { // scope + Attachment attachment{CLIENT, databaseUri, attachmentOptions}; + Transaction transaction{attachment}; + + Statement queryContext{ + attachment, transaction, "select rdb$get_context('SYSTEM', 'REPLICA_MODE') from rdb$database"}; + BOOST_REQUIRE(queryContext.execute(transaction)); + BOOST_REQUIRE(queryContext.getString(0).has_value()); + BOOST_CHECK_EQUAL(queryContext.getString(0).value(), "READ-ONLY"); + + Statement queryMon{attachment, transaction, "select mon$replica_mode from mon$database"}; + BOOST_REQUIRE(queryMon.execute(transaction)); + BOOST_CHECK_EQUAL(queryMon.getInt32(0).value(), 1); + } + + manager.execute(DatabaseManagerOptions().setDatabase(databasePath).setReplicaMode(ReplicaMode::NONE)); + + { // scope + Attachment attachment{CLIENT, databaseUri, attachmentOptions}; + Transaction transaction{attachment}; + + Statement queryContext{ + attachment, transaction, "select rdb$get_context('SYSTEM', 'REPLICA_MODE') from rdb$database"}; + BOOST_REQUIRE(queryContext.execute(transaction)); + BOOST_CHECK(!queryContext.getString(0).has_value()); + + Statement queryMon{attachment, transaction, "select mon$replica_mode from mon$database"}; + BOOST_REQUIRE(queryMon.execute(transaction)); + BOOST_CHECK_EQUAL(queryMon.getInt32(0).value(), 0); + } + + Attachment cleanup{CLIENT, databaseUri, attachmentOptions}; + cleanup.dropDatabase(); +} + +BOOST_AUTO_TEST_CASE(restoreWithReplicaMode) +{ + const auto sourceDatabasePath = getTempFile("DatabaseManager-restoreReplica-source.fdb", false); + const auto restoredDatabasePath = getTempFile("DatabaseManager-restoreReplica-restored.fdb", false); + const auto backupFile = getTempFile("DatabaseManager-restoreReplica.fbk", false); + const auto sourceDatabaseUri = getTempFile("DatabaseManager-restoreReplica-source.fdb"); + const auto restoredDatabaseUri = getTempFile("DatabaseManager-restoreReplica-restored.fdb"); + const auto attachmentOptions = AttachmentOptions().setConnectionCharSet("UTF8"); + + { // scope + Attachment attachment{ + CLIENT, sourceDatabaseUri, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + Transaction transaction{attachment}; + Statement create{attachment, transaction, "create table test(id integer)"}; + create.execute(transaction); + transaction.commit(); + } + + BackupManager backupManager{CLIENT, makeServiceManagerOptions()}; + backupManager.backup(BackupOptions().setDatabase(sourceDatabasePath).setBackupFile(backupFile)); + backupManager.restore(RestoreOptions() + .setDatabase(restoredDatabasePath) + .setBackupFile(backupFile) + .setReplicaMode(ReplicaMode::READ_WRITE)); + + { // scope + Attachment restored{CLIENT, restoredDatabaseUri, attachmentOptions}; + FbDropDatabase restoredDrop{restored}; + Transaction transaction{restored}; + + Statement queryContext{ + restored, transaction, "select rdb$get_context('SYSTEM', 'REPLICA_MODE') from rdb$database"}; + BOOST_REQUIRE(queryContext.execute(transaction)); + BOOST_REQUIRE(queryContext.getString(0).has_value()); + BOOST_CHECK_EQUAL(queryContext.getString(0).value(), "READ-WRITE"); + + Statement queryMon{restored, transaction, "select mon$replica_mode from mon$database"}; + BOOST_REQUIRE(queryMon.execute(transaction)); + BOOST_CHECK_EQUAL(queryMon.getInt32(0).value(), 2); + } + + Attachment cleanup{CLIENT, sourceDatabaseUri, attachmentOptions}; + cleanup.dropDatabase(); +} + +BOOST_AUTO_TEST_SUITE_END()