From f2bdc45f24696113fd714ad97ca9d9b860021c40 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Thu, 9 Apr 2026 11:28:47 -0300 Subject: [PATCH 1/2] Add BackupManager --- src/fb-cpp/BackupManager.cpp | 92 ++++++++++ src/fb-cpp/BackupManager.h | 329 ++++++++++++++++++++++++++++++++++ src/fb-cpp/ServiceManager.cpp | 196 ++++++++++++++++++++ src/fb-cpp/ServiceManager.h | 305 +++++++++++++++++++++++++++++++ src/fb-cpp/fb-cpp.h | 2 + src/test/BackupManager.cpp | 328 +++++++++++++++++++++++++++++++++ src/test/TestUtil.cpp | 15 +- src/test/TestUtil.h | 18 +- 8 files changed, 1278 insertions(+), 7 deletions(-) create mode 100644 src/fb-cpp/BackupManager.cpp create mode 100644 src/fb-cpp/BackupManager.h create mode 100644 src/fb-cpp/ServiceManager.cpp create mode 100644 src/fb-cpp/ServiceManager.h create mode 100644 src/test/BackupManager.cpp diff --git a/src/fb-cpp/BackupManager.cpp b/src/fb-cpp/BackupManager.cpp new file mode 100644 index 0000000..9aa38d9 --- /dev/null +++ b/src/fb-cpp/BackupManager.cpp @@ -0,0 +1,92 @@ +/* + * MIT License + * + * Copyright (c) 2026 Adriano dos Santos Fernandes + * + * 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 "BackupManager.h" +#include "Client.h" + +using namespace fbcpp; +using namespace fbcpp::impl; + +void BackupManager::backup(const BackupOptions& options) +{ + StatusWrapper statusWrapper{getClient()}; + auto builder = + fbUnique(getClient().getUtil()->getXpbBuilder(&statusWrapper, fb::IXpbBuilder::SPB_START, nullptr, 0)); + builder->insertTag(&statusWrapper, isc_action_svc_backup); + builder->insertString(&statusWrapper, isc_spb_dbname, options.getDatabase().c_str()); + + for (const auto& backupFile : options.getBackupFiles()) + { + builder->insertString(&statusWrapper, isc_spb_bkp_file, backupFile.path.c_str()); + + if (backupFile.length) + addSpbInt(builder.get(), &statusWrapper, isc_spb_bkp_length, *backupFile.length, "Backup file length"); + } + + if (options.getVerboseOutput()) + builder->insertTag(&statusWrapper, isc_spb_verbose); + + if (const auto parallelWorkers = options.getParallelWorkers()) + builder->insertInt(&statusWrapper, isc_spb_bkp_parallel_workers, static_cast(*parallelWorkers)); + + const auto buffer = builder->getBuffer(&statusWrapper); + const auto length = builder->getBufferLength(&statusWrapper); + + startAction(std::vector(buffer, buffer + length)); + waitForCompletion(options.getVerboseOutput()); +} + +void BackupManager::restore(const RestoreOptions& options) +{ + StatusWrapper statusWrapper{getClient()}; + auto builder = + fbUnique(getClient().getUtil()->getXpbBuilder(&statusWrapper, fb::IXpbBuilder::SPB_START, nullptr, 0)); + builder->insertTag(&statusWrapper, isc_action_svc_restore); + + for (const auto& databaseFile : options.getDatabaseFiles()) + { + builder->insertString(&statusWrapper, isc_spb_dbname, databaseFile.path.c_str()); + + if (databaseFile.length) + addSpbInt(builder.get(), &statusWrapper, isc_spb_res_length, *databaseFile.length, "Database file length"); + } + + for (const auto& backupFile : options.getBackupFiles()) + builder->insertString(&statusWrapper, isc_spb_bkp_file, backupFile.c_str()); + + builder->insertInt( + &statusWrapper, isc_spb_options, options.getReplace() ? isc_spb_res_replace : isc_spb_res_create); + + if (options.getVerboseOutput()) + builder->insertTag(&statusWrapper, isc_spb_verbose); + + if (const auto parallelWorkers = options.getParallelWorkers()) + builder->insertInt(&statusWrapper, isc_spb_res_parallel_workers, static_cast(*parallelWorkers)); + + const auto buffer = builder->getBuffer(&statusWrapper); + const auto length = builder->getBufferLength(&statusWrapper); + + startAction(std::vector(buffer, buffer + length)); + waitForCompletion(options.getVerboseOutput(), true); +} diff --git a/src/fb-cpp/BackupManager.h b/src/fb-cpp/BackupManager.h new file mode 100644 index 0000000..137c3d9 --- /dev/null +++ b/src/fb-cpp/BackupManager.h @@ -0,0 +1,329 @@ +/* + * MIT License + * + * Copyright (c) 2026 Adriano dos Santos Fernandes + * + * 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_BACKUP_MANAGER_H +#define FBCPP_BACKUP_MANAGER_H + +#include "ServiceManager.h" +#include +#include +#include +#include +#include + + +/// +/// fb-cpp namespace. +/// +namespace fbcpp +{ + /// + /// Represents options used to run a backup operation through the service manager. + /// + class BackupOptions final + { + public: + struct BackupFileSpec final + { + std::string path; + std::optional length; + }; + + public: + /// + /// Returns the database path to be backed up. + /// + const std::string& getDatabase() const + { + return database; + } + + /// + /// Sets the database path to be backed up. + /// + BackupOptions& setDatabase(const std::string& value) + { + database = value; + return *this; + } + + /// + /// Returns the configured backup file specifications. + /// + const std::vector& getBackupFiles() const + { + return backupFiles; + } + + /// + /// Appends a backup file path. + /// + BackupOptions& addBackupFile(const std::string& value) + { + backupFiles.emplace_back(value, std::nullopt); + return *this; + } + + /// + /// Appends a backup file path with a split length. + /// + BackupOptions& addBackupFile(const std::string& value, std::uint64_t length) + { + if (!backupFiles.empty() && !backupFiles.back().length) + throw std::invalid_argument{"Cannot add a backup file with length after a backup file without length"}; + + backupFiles.emplace_back(value, length); + return *this; + } + + /// + /// Replaces the backup file paths with a single path. + /// + BackupOptions& setBackupFile(const std::string& value) + { + backupFiles = {{value, std::nullopt}}; + return *this; + } + + /// + /// Replaces the backup file paths with a single path and split length. + /// + BackupOptions& setBackupFile(const std::string& value, std::uint64_t length) + { + backupFiles = {{value, length}}; + return *this; + } + + /// + /// Returns the verbose output callback. + /// + const ServiceManager::VerboseOutput& getVerboseOutput() const + { + return verboseOutput; + } + + /// + /// Sets the verbose output callback. + /// + BackupOptions& setVerboseOutput(ServiceManager::VerboseOutput value) + { + verboseOutput = std::move(value); + return *this; + } + + /// + /// Returns the requested number of parallel workers. + /// + const std::optional& getParallelWorkers() const + { + return parallelWorkers; + } + + /// + /// Sets the requested number of parallel workers. + /// + BackupOptions& setParallelWorkers(std::uint32_t value) + { + parallelWorkers = value; + return *this; + } + + private: + std::string database; + std::vector backupFiles; + ServiceManager::VerboseOutput verboseOutput; + std::optional parallelWorkers; + }; + + /// + /// Represents options used to run a restore operation through the service manager. + /// + class RestoreOptions final + { + public: + struct DatabaseFileSpec final + { + std::string path; + std::optional length; + }; + + public: + /// + /// Returns the configured database file specifications. + /// + const std::vector& getDatabaseFiles() const + { + return databaseFiles; + } + + /// + /// Sets the database path to be restored. + /// + RestoreOptions& setDatabase(const std::string& value) + { + databaseFiles = {{value, std::nullopt}}; + return *this; + } + + /// + /// Sets the database path to be restored with a split length. + /// + RestoreOptions& setDatabase(const std::string& value, std::uint64_t length) + { + databaseFiles = {{value, length}}; + return *this; + } + + /// + /// Appends a database file path. + /// + RestoreOptions& addDatabaseFile(const std::string& value) + { + databaseFiles.emplace_back(value, std::nullopt); + return *this; + } + + /// + /// Appends a database file path with a split length. + /// + RestoreOptions& addDatabaseFile(const std::string& value, std::uint64_t length) + { + if (!databaseFiles.empty() && !databaseFiles.back().length) + { + throw std::invalid_argument{ + "Cannot add a database file with length after a database file without length"}; + } + + databaseFiles.emplace_back(value, length); + return *this; + } + + /// + /// Returns the backup file paths. + /// + const std::vector& getBackupFiles() const + { + return backupFiles; + } + + /// + /// Appends a backup file path. + /// + RestoreOptions& addBackupFile(const std::string& value) + { + backupFiles.emplace_back(value); + return *this; + } + + /// + /// Replaces the backup file paths with a single path. + /// + RestoreOptions& setBackupFile(const std::string& value) + { + backupFiles = {value}; + return *this; + } + + /// + /// Returns whether the target database should be replaced. + /// + bool getReplace() const + { + return replace; + } + + /// + /// Sets whether the target database should be replaced. + /// + RestoreOptions& setReplace(bool value) + { + replace = value; + return *this; + } + + /// + /// Returns the verbose output callback. + /// + const ServiceManager::VerboseOutput& getVerboseOutput() const + { + return verboseOutput; + } + + /// + /// Sets the verbose output callback. + /// + RestoreOptions& setVerboseOutput(ServiceManager::VerboseOutput value) + { + verboseOutput = std::move(value); + return *this; + } + + /// + /// Returns the requested number of parallel workers. + /// + const std::optional& getParallelWorkers() const + { + return parallelWorkers; + } + + /// + /// Sets the requested number of parallel workers. + /// + RestoreOptions& setParallelWorkers(std::uint32_t value) + { + parallelWorkers = value; + return *this; + } + + private: + std::vector databaseFiles; + std::vector backupFiles; + bool replace = false; + ServiceManager::VerboseOutput verboseOutput; + std::optional parallelWorkers; + }; + + /// + /// Executes backup and restore operations through the Firebird service manager. + /// + class BackupManager final : public ServiceManager + { + public: + using ServiceManager::ServiceManager; + + public: + /// + /// Runs a backup operation using the provided options. + /// + void backup(const BackupOptions& options); + + /// + /// Runs a restore operation using the provided options. + /// + void restore(const RestoreOptions& options); + }; +} // namespace fbcpp + + +#endif // FBCPP_BACKUP_MANAGER_H diff --git a/src/fb-cpp/ServiceManager.cpp b/src/fb-cpp/ServiceManager.cpp new file mode 100644 index 0000000..2d41fd0 --- /dev/null +++ b/src/fb-cpp/ServiceManager.cpp @@ -0,0 +1,196 @@ +/* + * MIT License + * + * Copyright (c) 2026 Adriano dos Santos Fernandes + * + * 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 "ServiceManager.h" +#include "Client.h" +#include "Exception.h" +#include + +using namespace fbcpp; +using namespace fbcpp::impl; + + +static void emitVerboseChunk( + std::string& pendingLine, const std::string_view chunk, const ServiceManager::VerboseOutput& verboseOutput) +{ + if (!verboseOutput || chunk.empty()) + return; + + for (char ch : chunk) + { + if (ch == '\r') + continue; + + if (ch == '\n') + { + verboseOutput(pendingLine); + pendingLine.clear(); + } + else + pendingLine += ch; + } +} + + +ServiceManager::ServiceManager(Client& client, const ServiceManagerOptions& options) + : client{&client} +{ + const auto master = client.getMaster(); + StatusWrapper statusWrapper{client}; + + auto spbBuilder = fbUnique(client.getUtil()->getXpbBuilder(&statusWrapper, fb::IXpbBuilder::SPB_ATTACH, + reinterpret_cast(options.getSpb().data()), + static_cast(options.getSpb().size()))); + + if (const auto userName = options.getUserName()) + spbBuilder->insertString(&statusWrapper, isc_spb_user_name, userName->c_str()); + + if (const auto password = options.getPassword()) + spbBuilder->insertString(&statusWrapper, isc_spb_password, password->c_str()); + + if (const auto role = options.getRole()) + spbBuilder->insertString(&statusWrapper, isc_spb_sql_role_name, role->c_str()); + + auto dispatcher = fbRef(master->getDispatcher()); + const auto spbBuffer = spbBuilder->getBuffer(&statusWrapper); + const auto spbBufferLen = spbBuilder->getBufferLength(&statusWrapper); + auto service = options.getServiceManagerName(); + + if (const auto host = options.getServer()) + service = host.value() + ':' + service; + + handle.reset(dispatcher->attachServiceManager(&statusWrapper, service.c_str(), spbBufferLen, spbBuffer)); +} + +void ServiceManager::disconnect() +{ + detachHandle(); +} + +void ServiceManager::startAction(const std::vector& spb) +{ + assert(isValid()); + + StatusWrapper statusWrapper{*client}; + handle->start(&statusWrapper, static_cast(spb.size()), spb.data()); +} + +void ServiceManager::waitForCompletion(const VerboseOutput& verboseOutput, bool requestStdin) +{ + assert(isValid()); + + StatusWrapper statusWrapper{*client}; + auto receiveBuilder = + fbUnique(client->getUtil()->getXpbBuilder(&statusWrapper, fb::IXpbBuilder::SPB_RECEIVE, nullptr, 0)); + receiveBuilder->insertTag(&statusWrapper, verboseOutput ? isc_info_svc_line : isc_info_svc_to_eof); + + if (requestStdin) + receiveBuilder->insertTag(&statusWrapper, isc_info_svc_stdin); + + const auto receiveLength = receiveBuilder->getBufferLength(&statusWrapper); + const auto* receiveBuffer = receiveBuilder->getBuffer(&statusWrapper); + + std::vector buffer(16u * 1024u); + std::string pendingLine; + unsigned stdinRequest = 0; + + for (bool running = true; running;) + { + auto sendBuilder = + fbUnique(client->getUtil()->getXpbBuilder(&statusWrapper, fb::IXpbBuilder::SPB_SEND, nullptr, 0)); + + if (stdinRequest) + throw FbCppException("Service requested stdin input"); + + std::fill(buffer.begin(), buffer.end(), 0); + handle->query(&statusWrapper, sendBuilder->getBufferLength(&statusWrapper), + sendBuilder->getBuffer(&statusWrapper), receiveLength, receiveBuffer, static_cast(buffer.size()), + buffer.data()); + + auto responseBuilder = fbUnique(client->getUtil()->getXpbBuilder( + &statusWrapper, fb::IXpbBuilder::SPB_RESPONSE, buffer.data(), static_cast(buffer.size()))); + + stdinRequest = 0; + int outputLength = 0; + bool notReady = false; + + for (responseBuilder->rewind(&statusWrapper); running && !responseBuilder->isEof(&statusWrapper); + responseBuilder->moveNext(&statusWrapper)) + { + switch (responseBuilder->getTag(&statusWrapper)) + { + case isc_info_svc_line: + { + const auto* line = responseBuilder->getString(&statusWrapper); + const auto length = static_cast(responseBuilder->getLength(&statusWrapper)); + if (verboseOutput && length > 0) + verboseOutput(std::string_view{line, static_cast(length)}); + outputLength = length; + break; + } + + case isc_info_svc_to_eof: + { + const auto* bytes = reinterpret_cast(responseBuilder->getBytes(&statusWrapper)); + const auto length = static_cast(responseBuilder->getLength(&statusWrapper)); + emitVerboseChunk(pendingLine, std::string_view{bytes, static_cast(length)}, verboseOutput); + outputLength = length; + break; + } + + case isc_info_svc_stdin: + stdinRequest = static_cast(responseBuilder->getInt(&statusWrapper)); + break; + + case isc_info_end: + running = false; + break; + + case isc_info_truncated: + case isc_info_data_not_ready: + case isc_info_svc_timeout: + notReady = true; + break; + + default: + break; + } + } + + if (outputLength || stdinRequest || notReady) + running = true; + } + + if (verboseOutput && !pendingLine.empty()) + verboseOutput(pendingLine); +} + +void ServiceManager::detachHandle() +{ + assert(isValid()); + + StatusWrapper statusWrapper{*client}; + handle->detach(&statusWrapper); + handle.reset(); +} diff --git a/src/fb-cpp/ServiceManager.h b/src/fb-cpp/ServiceManager.h new file mode 100644 index 0000000..46d0440 --- /dev/null +++ b/src/fb-cpp/ServiceManager.h @@ -0,0 +1,305 @@ +/* + * MIT License + * + * Copyright (c) 2026 Adriano dos Santos Fernandes + * + * 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_SERVICE_MANAGER_H +#define FBCPP_SERVICE_MANAGER_H + +#include "Exception.h" +#include "fb-api.h" +#include "SmartPtrs.h" +#include +#include +#include +#include +#include +#include +#include + + +/// +/// fb-cpp namespace. +/// +namespace fbcpp +{ + class Client; + + /// + /// Represents options used when creating a ServiceManager object. + /// + class ServiceManagerOptions final + { + public: + /// + /// Returns the server used to attach to the service manager. + /// + const std::optional& getServer() const + { + return server; + } + + /// + /// Sets the server used to attach to the service manager. + /// + ServiceManagerOptions& setServer(const std::string& value) + { + server = value; + return *this; + } + + /// + /// Returns the service manager name. + /// + const std::string& getServiceManagerName() const + { + return serviceManagerName; + } + + /// + /// Sets the service manager name. + /// + ServiceManagerOptions& setServiceManagerName(const std::string& value) + { + serviceManagerName = value; + return *this; + } + + /// + /// Returns the user name used to attach to the service manager. + /// + const std::optional& getUserName() const + { + return userName; + } + + /// + /// Sets the user name used to attach to the service manager. + /// + ServiceManagerOptions& setUserName(const std::string& value) + { + userName = value; + return *this; + } + + /// + /// Returns the password used to attach to the service manager. + /// + const std::optional& getPassword() const + { + return password; + } + + /// + /// Sets the password used to attach to the service manager. + /// + ServiceManagerOptions& setPassword(const std::string& value) + { + password = value; + return *this; + } + + /// + /// Returns the role used to attach to the service manager. + /// + const std::optional& getRole() const + { + return role; + } + + /// + /// Sets the role used to attach to the service manager. + /// + ServiceManagerOptions& setRole(const std::string& value) + { + role = value; + return *this; + } + + /// + /// Returns the raw service attach SPB. + /// + const std::vector& getSpb() const + { + return spb; + } + + /// + /// Sets the raw service attach SPB. + /// + ServiceManagerOptions& setSpb(const std::vector& value) + { + spb = value; + return *this; + } + + /// + /// Sets the raw service attach SPB. + /// + ServiceManagerOptions& setSpb(std::vector&& value) + { + spb = std::move(value); + return *this; + } + + private: + std::optional server; + std::string serviceManagerName = "service_mgr"; + std::optional userName; + std::optional password; + std::optional role; + std::vector spb; + }; + + /// + /// Represents a connection to the Firebird service manager. + /// + class ServiceManager + { + public: + /// + /// Function invoked when a verbose service output line is available. + /// + using VerboseOutput = std::function; + + /// + /// Attaches to the service manager specified by the given options. + /// + explicit ServiceManager(Client& client, const ServiceManagerOptions& options = {}); + + /// + /// Move constructor. + /// A moved ServiceManager object becomes invalid. + /// + ServiceManager(ServiceManager&& o) noexcept + : client{o.client}, + handle{std::move(o.handle)} + { + } + + /// + /// @brief Transfers ownership of another ServiceManager into this one. + /// + /// The old handle is detached via `disconnect()`. + /// After the assignment, `this` is valid (with `o`'s handle) and `o` is invalid. + /// + ServiceManager& operator=(ServiceManager&& o) noexcept + { + if (this != &o) + { + if (isValid()) + { + try + { + detachHandle(); + } + catch (...) + { + // swallow + } + } + + client = o.client; + handle = std::move(o.handle); + } + + return *this; + } + + ServiceManager(const ServiceManager&) = delete; + ServiceManager& operator=(const ServiceManager&) = delete; + + /// + /// Detaches from the service manager. + /// + ~ServiceManager() noexcept + { + if (isValid()) + { + try + { + detachHandle(); + } + catch (...) + { + // swallow + } + } + } + + public: + /// + /// Returns whether the ServiceManager object is valid. + /// + bool isValid() noexcept + { + return handle != nullptr; + } + + /// + /// Returns the Client object reference used to create this ServiceManager object. + /// + Client& getClient() noexcept + { + return *client; + } + + /// + /// Returns the internal Firebird IService handle. + /// + FbRef getHandle() noexcept + { + return handle; + } + + /// + /// Detaches from the service manager. + /// + void disconnect(); + + protected: + static void addSpbInt(fb::IXpbBuilder* builder, impl::StatusWrapper* status, unsigned char tag, + std::uint64_t value, const char* what) + { + if (value > static_cast(std::numeric_limits::max())) + throw FbCppException(std::string(what) + " is too large"); + + if (value > static_cast(std::numeric_limits::max())) + builder->insertBigInt(status, tag, static_cast(value)); + else + builder->insertInt(status, tag, static_cast(value)); + } + + void startAction(const std::vector& spb); + void waitForCompletion(const VerboseOutput& verboseOutput = {}, bool requestStdin = false); + + private: + void detachHandle(); + + private: + Client* client; + FbRef handle; + }; +} // namespace fbcpp + + +#endif // FBCPP_SERVICE_MANAGER_H diff --git a/src/fb-cpp/fb-cpp.h b/src/fb-cpp/fb-cpp.h index b4fc185..a0fa3db 100644 --- a/src/fb-cpp/fb-cpp.h +++ b/src/fb-cpp/fb-cpp.h @@ -34,5 +34,7 @@ #include "Batch.h" #include "Blob.h" #include "EventListener.h" +#include "ServiceManager.h" +#include "BackupManager.h" #endif // FBCPP_H diff --git a/src/test/BackupManager.cpp b/src/test/BackupManager.cpp new file mode 100644 index 0000000..b97e086 --- /dev/null +++ b/src/test/BackupManager.cpp @@ -0,0 +1,328 @@ +/* + * MIT License + * + * Copyright (c) 2026 Adriano dos Santos Fernandes + * + * 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/Exception.h" +#include "fb-cpp/Statement.h" +#include "fb-cpp/Transaction.h" +#include +#include +#include +#include +#include +#include + + +namespace data = boost::unit_test::data; + + +namespace +{ + ServiceManagerOptions makeServiceManagerOptions() + { + auto options = ServiceManagerOptions{}; + + if (const auto server = getServer()) + options.setServer(server.value()); + + return options; + } + + struct BackupRestoreVerboseCase + { + const char* suffix; + bool backupVerbose; + bool restoreVerbose; + }; + + std::ostream& operator<<(std::ostream& os, const BackupRestoreVerboseCase& testCase) + { + return os << testCase.suffix; + } + + std::string normalizedFilename(const std::filesystem::path& path) + { +#ifdef _WIN32 + auto str = path.filename().string(); + std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return std::tolower(c); }); + return str; +#else + return path.filename().string(); +#endif + } + + static const std::initializer_list BACKUP_RESTORE_VERBOSE_CASES{ + {"quiet-quiet", false, false}, + {"verbose-quiet", true, false}, + {"quiet-verbose", false, true}, + {"verbose-verbose", true, true}, + }; +} // namespace + + +BOOST_AUTO_TEST_SUITE(BackupManagerSuite) + +BOOST_AUTO_TEST_CASE(backupOptionsManageBackupFiles) +{ + BackupOptions options; + options.addBackupFile("first.fbk").addBackupFile("second.fbk"); + BOOST_REQUIRE_EQUAL(options.getBackupFiles().size(), 2u); + BOOST_CHECK_EQUAL(options.getBackupFiles()[0].path, "first.fbk"); + BOOST_CHECK_EQUAL(options.getBackupFiles()[1].path, "second.fbk"); + + options.setBackupFile("only.fbk"); + BOOST_REQUIRE_EQUAL(options.getBackupFiles().size(), 1u); + BOOST_CHECK_EQUAL(options.getBackupFiles()[0].path, "only.fbk"); +} + +BOOST_AUTO_TEST_CASE(backupOptionsRejectLengthAfterItemWithoutLength) +{ + BackupOptions options; + options.addBackupFile("first.fbk"); + BOOST_CHECK_THROW(options.addBackupFile("second.fbk", 2048), std::invalid_argument); + + options.setBackupFile("only.fbk"); + BOOST_CHECK_THROW(options.addBackupFile("second.fbk", 2048), std::invalid_argument); + + BackupOptions validOptions; + validOptions.addBackupFile("first.fbk", 1024).addBackupFile("second.fbk", 2048); + BOOST_REQUIRE_EQUAL(validOptions.getBackupFiles().size(), 2u); +} + +BOOST_AUTO_TEST_CASE(restoreOptionsManageBackupFiles) +{ + RestoreOptions options; + options.addBackupFile("first.fbk").addBackupFile("second.fbk"); + BOOST_REQUIRE_EQUAL(options.getBackupFiles().size(), 2u); + BOOST_CHECK_EQUAL(options.getBackupFiles()[0], "first.fbk"); + BOOST_CHECK_EQUAL(options.getBackupFiles()[1], "second.fbk"); + + options.setBackupFile("only.fbk"); + BOOST_REQUIRE_EQUAL(options.getBackupFiles().size(), 1u); + BOOST_CHECK_EQUAL(options.getBackupFiles()[0], "only.fbk"); +} + +BOOST_AUTO_TEST_CASE(restoreOptionsRejectLengthAfterItemWithoutLength) +{ + RestoreOptions options; + options.addDatabaseFile("first.fdb"); + BOOST_CHECK_THROW(options.addDatabaseFile("second.fdb", 2048), std::invalid_argument); + + options.setDatabase("only.fdb"); + BOOST_CHECK_THROW(options.addDatabaseFile("second.fdb", 2048), std::invalid_argument); + + RestoreOptions validOptions; + validOptions.addDatabaseFile("first.fdb", 1024).addDatabaseFile("second.fdb", 2048); + BOOST_REQUIRE_EQUAL(validOptions.getDatabaseFiles().size(), 2u); +} + +BOOST_AUTO_TEST_CASE(serviceManagerDisconnectAndMove) +{ + ServiceManager manager1{CLIENT, makeServiceManagerOptions()}; + BOOST_CHECK(manager1.isValid()); + + auto manager2 = std::move(manager1); + BOOST_CHECK(manager2.isValid()); + BOOST_CHECK(!manager1.isValid()); + + manager2.disconnect(); + BOOST_CHECK(!manager2.isValid()); +} + +BOOST_DATA_TEST_CASE(backupAndRestoreRoundTrip, data::make(BACKUP_RESTORE_VERBOSE_CASES), testCase) +{ + const auto attachmentOptions = AttachmentOptions().setConnectionCharSet("UTF8"); + const auto prefix = std::string("BackupManager-backupAndRestoreRoundTrip-") + testCase.suffix; + const auto sourceDatabasePath = getTempFile(prefix + "-source.fdb", false); + const auto restoredDatabasePath = getTempFile(prefix + "-restored.fdb", false); + const auto backupFile = getTempFile(prefix + ".fbk", false); + const auto sourceDatabaseUri = getTempFile(prefix + "-source.fdb"); + const auto restoredDatabaseUri = getTempFile(prefix + "-restored.fdb"); + + { // scope + Attachment attachment{ + CLIENT, sourceDatabaseUri, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; + + Transaction transaction{attachment}; + + Statement create{ + attachment, transaction, "create table test(id integer not null primary key, name varchar(20))"}; + create.execute(transaction); + transaction.commitRetaining(); + + Statement insert{attachment, transaction, "insert into test(id, name) values (1, 'backup')"}; + insert.execute(transaction); + transaction.commit(); + } + + BackupManager manager{CLIENT, makeServiceManagerOptions()}; + std::vector backupVerboseLines; + auto backupOptions = BackupOptions().setDatabase(sourceDatabasePath).setBackupFile(backupFile); + + if (testCase.backupVerbose) + backupOptions.setVerboseOutput([&](const std::string_view line) { backupVerboseLines.emplace_back(line); }); + + manager.backup(backupOptions); + BOOST_CHECK_EQUAL(!backupVerboseLines.empty(), testCase.backupVerbose); + + std::vector restoreVerboseLines; + auto restoreOptions = RestoreOptions().setDatabase(restoredDatabasePath).setBackupFile(backupFile); + + if (testCase.restoreVerbose) + restoreOptions.setVerboseOutput([&](const std::string_view line) { restoreVerboseLines.emplace_back(line); }); + + manager.restore(restoreOptions); + BOOST_CHECK_EQUAL(!restoreVerboseLines.empty(), testCase.restoreVerbose); + + Attachment restored{CLIENT, restoredDatabaseUri, attachmentOptions}; + FbDropDatabase restoredDrop{restored}; + Transaction transaction{restored}; + Statement query{restored, transaction, "select id, name from test"}; + BOOST_REQUIRE(query.execute(transaction)); + BOOST_CHECK_EQUAL(query.getInt32(0).value(), 1); + BOOST_CHECK_EQUAL(query.getString(1).value(), "backup"); + transaction.commit(); + + Attachment cleanup{CLIENT, sourceDatabaseUri, attachmentOptions}; + cleanup.dropDatabase(); +} + +BOOST_AUTO_TEST_CASE(restoreReplace) +{ + const auto sourceDatabasePath = getTempFile("BackupManager-restoreReplace-source.fdb", false); + const auto restoredDatabasePath = getTempFile("BackupManager-restoreReplace-restored.fdb", false); + const auto backupFile = getTempFile("BackupManager-restoreReplace.fbk", false); + const auto sourceDatabaseUri = getTempFile("BackupManager-restoreReplace-source.fdb"); + const auto restoredDatabaseUri = getTempFile("BackupManager-restoreReplace-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 not null primary key)"}; + create.execute(transaction); + transaction.commitRetaining(); + + Statement insert{attachment, transaction, "insert into test(id) values (7)"}; + insert.execute(transaction); + transaction.commit(); + } + + BackupManager manager{CLIENT, makeServiceManagerOptions()}; + manager.backup(BackupOptions().setDatabase(sourceDatabasePath).setBackupFile(backupFile)); + manager.restore(RestoreOptions().setDatabase(restoredDatabasePath).setBackupFile(backupFile)); + manager.restore(RestoreOptions().setDatabase(restoredDatabasePath).setBackupFile(backupFile).setReplace(true)); + + Attachment restored{CLIENT, restoredDatabaseUri, attachmentOptions}; + FbDropDatabase restoredDrop{restored}; + Transaction transaction{restored}; + Statement query{restored, transaction, "select id from test"}; + BOOST_REQUIRE(query.execute(transaction)); + BOOST_CHECK_EQUAL(query.getInt32(0).value(), 7); + transaction.commit(); + + Attachment cleanup{CLIENT, sourceDatabaseUri, attachmentOptions}; + cleanup.dropDatabase(); +} + +BOOST_AUTO_TEST_CASE(multiFileDatabaseAndBackupRoundTrip) +{ + const auto sourceDatabasePath = getTempFile("BackupManager-multiFile-source.fdb", false); + const auto sourceSecondaryPath = getTempFile("BackupManager-multiFile-source-2.fdb", false); + const auto restoredDatabasePath = getTempFile("BackupManager-multiFile-restored.fdb", false); + const auto restoredSecondaryPath = getTempFile("BackupManager-multiFile-restored-2.fdb", false); + const auto backupFile1 = getTempFile("BackupManager-multiFile-1.fbk", false); + const auto backupFile2 = getTempFile("BackupManager-multiFile-2.fbk", false); + const auto sourceDatabaseUri = getTempFile("BackupManager-multiFile-source.fdb"); + const auto restoredDatabaseUri = getTempFile("BackupManager-multiFile-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 not null primary key, name varchar(50))"}; + create.execute(transaction); + transaction.commitRetaining(); + + Statement addFile{ + attachment, transaction, "alter database add file '" + sourceSecondaryPath + "' starting at page 241"}; + addFile.execute(transaction); + transaction.commitRetaining(); + + for (int i = 1; i <= 200; ++i) + { + Statement insert{attachment, transaction, "insert into test(id, name) values (?, ?)"}; + insert.setInt32(0, i); + insert.setString(1, "row-" + std::to_string(i) + std::string(40, 'x')); + insert.execute(transaction); + } + transaction.commitRetaining(); + + Statement queryFiles{attachment, transaction, + "select count(*), min(trim(rdb$file_name)) from rdb$files where rdb$file_sequence = 1"}; + BOOST_REQUIRE(queryFiles.execute(transaction)); + BOOST_CHECK_EQUAL(queryFiles.getInt64(0).value(), 1); + BOOST_CHECK_EQUAL(normalizedFilename(queryFiles.getString(1).value()), normalizedFilename(sourceSecondaryPath)); + transaction.commit(); + } + + BackupManager manager{CLIENT, makeServiceManagerOptions()}; + manager.backup( + BackupOptions().setDatabase(sourceDatabasePath).addBackupFile(backupFile1, 2048).addBackupFile(backupFile2)); + + manager.restore(RestoreOptions() + .setDatabase(restoredDatabasePath, 200) + .addDatabaseFile(restoredSecondaryPath) + .addBackupFile(backupFile1) + .addBackupFile(backupFile2)); + + Attachment restored{CLIENT, restoredDatabaseUri, attachmentOptions}; + FbDropDatabase restoredDrop{restored}; + Transaction transaction{restored}; + Statement query{restored, transaction, "select count(*), min(id), max(id) from test"}; + BOOST_REQUIRE(query.execute(transaction)); + BOOST_CHECK_EQUAL(query.getInt64(0).value(), 200); + BOOST_CHECK_EQUAL(query.getInt32(1).value(), 1); + BOOST_CHECK_EQUAL(query.getInt32(2).value(), 200); + + Statement queryFiles{ + restored, transaction, "select count(*), min(trim(rdb$file_name)) from rdb$files where rdb$file_sequence = 1"}; + BOOST_REQUIRE(queryFiles.execute(transaction)); + BOOST_CHECK_EQUAL(queryFiles.getInt64(0).value(), 1); + BOOST_CHECK_EQUAL(normalizedFilename(queryFiles.getString(1).value()), normalizedFilename(restoredSecondaryPath)); + transaction.commit(); + + Attachment cleanup{CLIENT, sourceDatabaseUri, attachmentOptions}; + cleanup.dropDatabase(); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/TestUtil.cpp b/src/test/TestUtil.cpp index 1c667d4..e4adad4 100644 --- a/src/test/TestUtil.cpp +++ b/src/test/TestUtil.cpp @@ -89,20 +89,27 @@ namespace fbcpp::test ~GlobalFixture() { + CLIENT.shutdown(); + if (removeTempDir) { std::error_code ec; fs::remove(tempDir, ec); } - - CLIENT.shutdown(); } }; } // namespace - std::string getTempFile(const std::string_view name) + std::string getTempFile(const std::string_view name, bool includeServerPrefix) + { + const auto path = (tempDir / name).string(); + return includeServerPrefix ? testServerPrefix + path : path; + } + + std::optional getServer() { - return testServerPrefix + (tempDir / name).string(); + return testServerPrefix.empty() ? std::nullopt + : std::optional{testServerPrefix.substr(0, testServerPrefix.size() - 1u)}; } } // namespace fbcpp::test diff --git a/src/test/TestUtil.h b/src/test/TestUtil.h index 7941f58..ffd4ff8 100644 --- a/src/test/TestUtil.h +++ b/src/test/TestUtil.h @@ -27,6 +27,7 @@ #include "fb-cpp/Attachment.h" #include "fb-cpp/Client.h" +#include #include #include #include @@ -38,7 +39,8 @@ namespace fbcpp::test { extern Client CLIENT; - std::string getTempFile(const std::string_view name); + std::optional getServer(); + std::string getTempFile(const std::string_view name, bool includeServerPrefix = true); class FbDropDatabase { @@ -48,9 +50,19 @@ namespace fbcpp::test { } - ~FbDropDatabase() + ~FbDropDatabase() noexcept { - attachment.dropDatabase(); + if (attachment.isValid()) + { + try + { + attachment.dropDatabase(); + } + catch (...) + { + // Ignore cleanup failures in test teardown. + } + } } FbDropDatabase(const FbDropDatabase&) = delete; From 47a71b65f7a7d7b8515526fe9ccbdfc11b7aeb5d Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Thu, 9 Apr 2026 12:26:06 -0300 Subject: [PATCH 2/2] Use clang-format-20 --- .github/workflows/main.yml | 2 +- .github/workflows/pull-request.yml | 2 +- check-format.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dbc5d84..91fb8cd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: - name: Install clang-format run: | sudo apt-get update - sudo apt-get install -y clang-format + sudo apt-get install -y clang-format-20 - name: Check clang-format run: | diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5b8a98d..f9866df 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -22,7 +22,7 @@ jobs: - name: Install clang-format run: | sudo apt-get update - sudo apt-get install -y clang-format + sudo apt-get install -y clang-format-20 - name: Check clang-format run: | diff --git a/check-format.sh b/check-format.sh index fc76f47..b00914b 100755 --- a/check-format.sh +++ b/check-format.sh @@ -2,7 +2,7 @@ failed_files=$( \ find src -type f \( -name "*.cpp" -o -name "*.h" \) \ - -exec sh -c 'for f; do [ "$(clang-format "$f")" != "$(cat "$f")" ] && echo "$f"; done' _ {} +) + -exec sh -c 'for f; do [ "$(clang-format-20 "$f")" != "$(cat "$f")" ] && echo "$f"; done' _ {} +) if [ -n "$failed_files" ]; then echo "The following files are not properly formatted:"