Program Listing for File RocksDbBackend.cpp

Return to documentation for file (include\util\db\backends\RocksDbBackend.cpp)

#include "RocksDbBackend.hpp"

#include <rocksdb/db.h>
#include <rocksdb/options.h>

#include <cstring>
#include <memory>
#include <string>
#include <utility>
#include <vector>

namespace PDJE_UTIL::db::backends {

namespace {

rocksdb::Slice
slice_of(std::string_view value)
{
    return rocksdb::Slice(value.data(), value.size());
}

} // namespace

class RocksDbBackend::Impl {
  public:
    ~Impl()
    {
        delete db_;
    }

    common::Result<void>
    open(const config_type &cfg)
    {
        if (db_ != nullptr) {
            return common::Result<void>::failure(
                { common::StatusCode::invalid_argument,
                  "RocksDB backend is already open." });
        }
        if (cfg.path.empty()) {
            return common::Result<void>::failure(
                { common::StatusCode::invalid_argument,
                  "RocksDbConfig.path must not be empty." });
        }

        config_ = cfg;
        if (config_.open_options.truncate_if_exists) {
            auto destroyed = RocksDbBackend::destroy(config_);
            if (!destroyed.ok()) {
                return destroyed;
            }
        }

        const bool exists = std::filesystem::exists(config_.path);
        if (!exists && !config_.open_options.create_if_missing &&
            !config_.open_options.read_only) {
            return common::Result<void>::failure(
                { common::StatusCode::not_found,
                  "RocksDB directory does not exist." });
        }

        auto options              = db_options_;
        options.create_if_missing = config_.open_options.create_if_missing;
        options.error_if_exists   = false;

        rocksdb::Status status;
        if (config_.open_options.read_only) {
            status = rocksdb::DB::OpenForReadOnly(
                options, config_.path.string(), &db_);
        } else {
            status = rocksdb::DB::Open(options, config_.path.string(), &db_);
        }

        if (!status.ok()) {
            db_ = nullptr;
            return common::Result<void>::failure(
                { common::StatusCode::backend_error, status.ToString() });
        }

        read_options_  = {};
        write_options_ = {};
        return common::Result<void>::success();
    }

    common::Result<void>
    close()
    {
        delete db_;
        db_            = nullptr;
        config_        = {};
        db_options_    = {};
        read_options_  = {};
        write_options_ = {};
        return common::Result<void>::success();
    }

    common::Result<bool>
    contains(std::string_view key) const
    {
        if (auto status = require_open(); !status.ok()) {
            return common::Result<bool>::failure(status);
        }

        std::string value;
        auto        get_status = db_->Get(read_options_, slice_of(key), &value);
        if (get_status.IsNotFound()) {
            return common::Result<bool>::success(false);
        }
        if (!get_status.ok()) {
            return common::Result<bool>::failure(
                { common::StatusCode::backend_error, get_status.ToString() });
        }
        return common::Result<bool>::success(true);
    }

    common::Result<Text>
    get_text(std::string_view key) const
    {
        auto raw = get_raw(key);
        if (!raw.ok()) {
            return common::Result<Text>::failure(raw.status());
        }
        if (raw.value().empty() || raw.value().front() != 'T') {
            return common::Result<Text>::failure(
                { common::StatusCode::type_mismatch,
                  "RocksDB value is not stored as text." });
        }
        return common::Result<Text>::success(
            std::string(raw.value().begin() + 1, raw.value().end()));
    }

    common::Result<Bytes>
    get_bytes(std::string_view key) const
    {
        auto raw = get_raw(key);
        if (!raw.ok()) {
            return common::Result<Bytes>::failure(raw.status());
        }
        if (raw.value().empty() || raw.value().front() != 'B') {
            return common::Result<Bytes>::failure(
                { common::StatusCode::type_mismatch,
                  "RocksDB value is not stored as bytes." });
        }

        Bytes bytes(raw.value().size() - 1);
        if (!bytes.empty()) {
            std::memcpy(bytes.data(), raw.value().data() + 1, bytes.size());
        }
        return common::Result<Bytes>::success(std::move(bytes));
    }

    common::Result<void>
    put_text(std::string_view key, std::string_view value)
    {
        if (auto status = require_writable(); !status.ok()) {
            return common::Result<void>::failure(status);
        }

        std::string encoded;
        encoded.reserve(value.size() + 1);
        encoded.push_back('T');
        encoded.append(value.data(), value.size());

        auto put_status =
            db_->Put(write_options_, slice_of(key), rocksdb::Slice(encoded));
        if (!put_status.ok()) {
            return common::Result<void>::failure(
                { common::StatusCode::backend_error, put_status.ToString() });
        }
        return common::Result<void>::success();
    }

    common::Result<void>
    put_bytes(std::string_view key, std::span<const std::byte> value)
    {
        if (auto status = require_writable(); !status.ok()) {
            return common::Result<void>::failure(status);
        }

        std::string encoded;
        encoded.reserve(value.size_bytes() + 1);
        encoded.push_back('B');
        encoded.append(reinterpret_cast<const char *>(value.data()),
                       value.size_bytes());

        auto put_status =
            db_->Put(write_options_, slice_of(key), rocksdb::Slice(encoded));
        if (!put_status.ok()) {
            return common::Result<void>::failure(
                { common::StatusCode::backend_error, put_status.ToString() });
        }
        return common::Result<void>::success();
    }

    common::Result<void>
    erase(std::string_view key)
    {
        if (auto status = require_writable(); !status.ok()) {
            return common::Result<void>::failure(status);
        }

        auto delete_status = db_->Delete(write_options_, slice_of(key));
        if (!delete_status.ok()) {
            return common::Result<void>::failure(
                { common::StatusCode::backend_error,
                  delete_status.ToString() });
        }
        return common::Result<void>::success();
    }

    common::Result<std::vector<Key>>
    list_keys(std::string_view prefix) const
    {
        if (auto status = require_open(); !status.ok()) {
            return common::Result<std::vector<Key>>::failure(status);
        }

        std::vector<Key>                   keys;
        std::unique_ptr<rocksdb::Iterator> iterator(
            db_->NewIterator(read_options_));
        for (iterator->Seek(slice_of(prefix)); iterator->Valid();
             iterator->Next()) {
            const auto current = iterator->key().ToStringView();
            if (!prefix.empty() && !current.starts_with(prefix)) {
                break;
            }
            keys.emplace_back(current);
        }

        if (!iterator->status().ok()) {
            return common::Result<std::vector<Key>>::failure(
                { common::StatusCode::backend_error,
                  iterator->status().ToString() });
        }

        return common::Result<std::vector<Key>>::success(std::move(keys));
    }

    common::Status
    require_open() const
    {
        if (db_ == nullptr) {
            return { common::StatusCode::closed,
                     "RocksDB backend is not open." };
        }
        return {};
    }

    common::Status
    require_writable() const
    {
        if (auto status = require_open(); !status.ok()) {
            return status;
        }
        if (config_.open_options.read_only) {
            return { common::StatusCode::unsupported,
                     "RocksDB backend is opened read-only." };
        }
        return {};
    }

    common::Result<std::vector<char>>
    get_raw(std::string_view key) const
    {
        if (auto status = require_open(); !status.ok()) {
            return common::Result<std::vector<char>>::failure(status);
        }

        std::string value;
        auto        get_status = db_->Get(read_options_, slice_of(key), &value);
        if (get_status.IsNotFound()) {
            return common::Result<std::vector<char>>::failure(
                { common::StatusCode::not_found,
                  "RocksDB key was not found." });
        }
        if (!get_status.ok()) {
            return common::Result<std::vector<char>>::failure(
                { common::StatusCode::backend_error, get_status.ToString() });
        }

        return common::Result<std::vector<char>>::success(
            std::vector<char>(value.begin(), value.end()));
    }

  private:
    config_type           config_{};
    rocksdb::Options      db_options_{};
    rocksdb::ReadOptions  read_options_{};
    rocksdb::WriteOptions write_options_{};
    rocksdb::DB          *db_ = nullptr;
};

RocksDbBackend::RocksDbBackend() : impl_(std::make_unique<Impl>())
{
}

RocksDbBackend::~RocksDbBackend() = default;

RocksDbBackend::RocksDbBackend(RocksDbBackend &&other) noexcept = default;

RocksDbBackend &
RocksDbBackend::operator=(RocksDbBackend &&other) noexcept = default;

common::Result<void>
RocksDbBackend::create(const config_type &cfg)
{
    if (cfg.path.empty()) {
        return common::Result<void>::failure(
            { common::StatusCode::invalid_argument,
              "RocksDbConfig.path must not be empty." });
    }

    rocksdb::Options options;
    options.create_if_missing = true;
    options.error_if_exists   = false;

    rocksdb::DB *db  = nullptr;
    auto open_status = rocksdb::DB::Open(options, cfg.path.string(), &db);
    if (!open_status.ok()) {
        return common::Result<void>::failure(
            { common::StatusCode::backend_error, open_status.ToString() });
    }

    delete db;
    return common::Result<void>::success();
}

common::Result<void>
RocksDbBackend::destroy(const config_type &cfg)
{
    if (cfg.path.empty()) {
        return common::Result<void>::failure(
            { common::StatusCode::invalid_argument,
              "RocksDbConfig.path must not be empty." });
    }

    std::error_code ec;
    std::filesystem::remove_all(cfg.path, ec);
    if (ec) {
        return common::Result<void>::failure(
            { common::StatusCode::io_error, ec.message() });
    }
    return common::Result<void>::success();
}

common::Result<void>
RocksDbBackend::open(const config_type &cfg)
{
    if (impl_ == nullptr) {
        impl_ = std::make_unique<Impl>();
    }
    return impl_->open(cfg);
}

common::Result<void>
RocksDbBackend::close()
{
    if (impl_ == nullptr) {
        return common::Result<void>::success();
    }
    return impl_->close();
}

common::Result<bool>
RocksDbBackend::contains(std::string_view key) const
{
    if (impl_ == nullptr) {
        return common::Result<bool>::failure(
            { common::StatusCode::closed, "RocksDB backend is not open." });
    }
    return impl_->contains(key);
}

common::Result<Text>
RocksDbBackend::get_text(std::string_view key) const
{
    if (impl_ == nullptr) {
        return common::Result<Text>::failure(
            { common::StatusCode::closed, "RocksDB backend is not open." });
    }
    return impl_->get_text(key);
}

common::Result<Bytes>
RocksDbBackend::get_bytes(std::string_view key) const
{
    if (impl_ == nullptr) {
        return common::Result<Bytes>::failure(
            { common::StatusCode::closed, "RocksDB backend is not open." });
    }
    return impl_->get_bytes(key);
}

common::Result<void>
RocksDbBackend::put_text(std::string_view key, std::string_view value)
{
    if (impl_ == nullptr) {
        return common::Result<void>::failure(
            { common::StatusCode::closed, "RocksDB backend is not open." });
    }
    return impl_->put_text(key, value);
}

common::Result<void>
RocksDbBackend::put_bytes(std::string_view           key,
                          std::span<const std::byte> value)
{
    if (impl_ == nullptr) {
        return common::Result<void>::failure(
            { common::StatusCode::closed, "RocksDB backend is not open." });
    }
    return impl_->put_bytes(key, value);
}

common::Result<void>
RocksDbBackend::erase(std::string_view key)
{
    if (impl_ == nullptr) {
        return common::Result<void>::failure(
            { common::StatusCode::closed, "RocksDB backend is not open." });
    }
    return impl_->erase(key);
}

common::Result<std::vector<Key>>
RocksDbBackend::list_keys(std::string_view prefix) const
{
    if (impl_ == nullptr) {
        return common::Result<std::vector<Key>>::failure(
            { common::StatusCode::closed, "RocksDB backend is not open." });
    }
    return impl_->list_keys(prefix);
}

} // namespace PDJE_UTIL::db::backends