Skip to content
Snippets Groups Projects
Select Git revision
  • 7daa47c318aed09ad96456fba39f9b38c2364fe2
  • main default protected
  • f-sss4grpc
  • dev
  • 108-implement-rpc-call-for-server-side-scripting
  • f-windows-conan-create
  • f-to-string
  • f-update-requirements
  • f-related-projects
  • f-role
  • f-remote-path
  • f-rel-path
  • f-consol-message
  • v0.3.0
  • v0.2.2
  • v0.2.1
  • v0.2.0
  • v0.1.2
  • v0.1.1
  • v0.1
  • v0.0.19
  • v0.0.18
  • v0.0.16
  • v0.0.15
  • v0.0.10
  • v0.0.9
  • v0.0.8
  • v0.0.7
  • v0.0.6
  • v0.0.5
  • v0.0.4
  • v0.0.3
  • v0.0.2
33 results

transaction.h

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    transaction.h 19.10 KiB
    /*
     * This file is a part of the CaosDB Project.
     *
     * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
     * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
     *
     * This program is free software: you can redistribute it and/or modify
     * it under the terms of the GNU Affero General Public License as
     * published by the Free Software Foundation, either version 3 of the
     * License, or (at your option) any later version.
     *
     * This program 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 Affero General Public License for more details.
     *
     * You should have received a copy of the GNU Affero General Public License
     * along with this program. If not, see <https://www.gnu.org/licenses/>.
     *
     */
    #ifndef CAOSDB_TRANSACTION_H
    #define CAOSDB_TRANSACTION_H
    #include "boost/log/core/record.hpp"                  // for record
    #include "boost/log/sources/record_ostream.hpp"       // for basic_record_o...
    #include "boost/preprocessor/seq/limits/enum_256.hpp" // for BOOST_PP_SEQ_E...
    #include "boost/preprocessor/seq/limits/size_256.hpp" // for BOOST_PP_SEQ_S...
    #include "caosdb/entity.h"                            // for Entity
    #include "caosdb/logging.h"
    #include "caosdb/entity/v1alpha1/main.grpc.pb.h" // for EntityTransactionSe...
    #include "caosdb/entity/v1alpha1/main.pb.h"      // for Entity, RetrieveReq...
    #include "caosdb/transaction_status.h"           // for TransactionStatus
    #include "caosdb/status_code.h"                  // for StatusCode
    #include "google/protobuf/util/json_util.h" // for MessageToJsonString, Jso...
    #include <iterator>
    // IWYU pragma: no_include <ext/alloc_traits.h>
    #include <memory> // for shared_ptr, unique_ptr
    #include <stdexcept>
    #include <string> // for string
    #include <vector> // for vector
    
    /**
     * Do all necessary checks and assure that another retrieval (by id or by
     * query) can be added as a sub-request to a transaction.
     */
    #define ASSERT_CAN_ADD_RETRIEVAL                                               \
      if (!IsStatus(TransactionStatus::INITIAL()) &&                               \
          !IsStatus(TransactionStatus::GO_ON())) {                                 \
        return StatusCode::TRANSACTION_STATUS_ERROR;                               \
      }                                                                            \
      switch (this->transaction_type) {                                            \
      case NONE:                                                                   \
        this->transaction_type = TransactionType::READ_ONLY;                       \
        break;                                                                     \
      case READ_ONLY:                                                              \
        break;                                                                     \
      case MIXED_READ_AND_WRITE:                                                   \
        break;                                                                     \
      default:                                                                     \
        CAOSDB_LOG_ERROR_AND_RETURN_STATUS(                                        \
          logger_name, StatusCode::TRANSACTION_TYPE_ERROR,                         \
          "You cannot add a retrieval to this transaction because it has the "     \
          "wrong TransactionType.")                                                \
      }
    
    /**
     * Do all necessary checks and assure that another retrieval (by id or by
     * query) can be added as a sub-request to a transaction.
     */
    #define ASSERT_CAN_ADD_QUERY                                                   \
      ASSERT_CAN_ADD_RETRIEVAL                                                     \
      if (this->has_query) {                                                       \
        CAOSDB_LOG_ERROR_AND_RETURN_STATUS(                                        \
          logger_name, StatusCode::UNSUPPORTED_FEATURE,                            \
          "Currently the number of queries which can be processed in a single "    \
          "transaction is limitted to one.");                                      \
      }
    
    /**
     * Do all necessary checks and assure that another deletion can be added as a
     * sub-request to a transaction.
     */
    #define ASSERT_CAN_ADD_DELETION                                                \
      if (!IsStatus(TransactionStatus::INITIAL()) &&                               \
          !IsStatus(TransactionStatus::GO_ON())) {                                 \
        return StatusCode::TRANSACTION_STATUS_ERROR;                               \
      }                                                                            \
      switch (this->transaction_type) {                                            \
      case NONE:                                                                   \
        this->transaction_type = TransactionType::DELETE;                          \
      case DELETE:                                                                 \
      case MIXED_WRITE:                                                            \
      case MIXED_READ_AND_WRITE:                                                   \
        break;                                                                     \
      default:                                                                     \
        CAOSDB_LOG_ERROR_AND_RETURN_STATUS(                                        \
          logger_name, StatusCode::TRANSACTION_TYPE_ERROR,                         \
          "You cannot add a deletion to this transaction because it has the "      \
          "wrong TransactionType.")                                                \
      }
    
    /**
     * Do all necessary checks and assure that another insertion can be added as a
     * sub-request to a transaction.
     */
    #define ASSERT_CAN_ADD_INSERTION                                               \
      if (!IsStatus(TransactionStatus::INITIAL()) &&                               \
          !IsStatus(TransactionStatus::GO_ON())) {                                 \
        return StatusCode::TRANSACTION_STATUS_ERROR;                               \
      }                                                                            \
      switch (this->transaction_type) {                                            \
      case NONE:                                                                   \
        this->transaction_type = TransactionType::INSERT;                          \
      case INSERT:                                                                 \
      case MIXED_WRITE:                                                            \
      case MIXED_READ_AND_WRITE:                                                   \
        break;                                                                     \
      default:                                                                     \
        CAOSDB_LOG_ERROR_AND_RETURN_STATUS(                                        \
          logger_name, StatusCode::TRANSACTION_TYPE_ERROR,                         \
          "You cannot add an insertion to this transaction because it has the "    \
          "wrong TransactionType.")                                                \
      }
    
    /**
     * Do all necessary checks and assure that another update can be added as a
     * sub-request to a transaction.
     */
    #define ASSERT_CAN_ADD_UPDATE                                                  \
      if (!IsStatus(TransactionStatus::INITIAL()) &&                               \
          !IsStatus(TransactionStatus::GO_ON())) {                                 \
        return StatusCode::TRANSACTION_STATUS_ERROR;                               \
      }                                                                            \
      switch (this->transaction_type) {                                            \
      case NONE:                                                                   \
        this->transaction_type = TransactionType::INSERT;                          \
      case INSERT:                                                                 \
      case MIXED_WRITE:                                                            \
      case MIXED_READ_AND_WRITE:                                                   \
        break;                                                                     \
      default:                                                                     \
        CAOSDB_LOG_ERROR_AND_RETURN_STATUS(                                        \
          logger_name, StatusCode::TRANSACTION_TYPE_ERROR,                         \
          "You cannot add an update to this transaction because it has the "       \
          "wrong TransactionType.")                                                \
      }                                                                            \
      if (!entity->HasId()) {                                                      \
        CAOSDB_LOG_ERROR_AND_RETURN_STATUS(                                        \
          logger_name, StatusCode::ORIGINAL_ENTITY_MISSING_ID,                     \
          "You cannot update this entity without any id. Probably you did not "    \
          "retrieve it first? Entity updates should always start with the "        \
          "retrieval of the existing entity which may then be changed.")           \
      }
    
    /**
     * @brief Creation and execution of transactions.
     * @author Timm Fitschen
     * @date 2021-08-05
     */
    namespace caosdb::transaction {
    using caosdb::entity::Entity;
    using ProtoEntity = caosdb::entity::v1alpha1::Entity;
    using caosdb::entity::v1alpha1::EntityTransactionService;
    using caosdb::entity::v1alpha1::FileDownloadResponse;
    using caosdb::entity::v1alpha1::FileTransmissionService;
    using caosdb::entity::v1alpha1::FileUploadRequest;
    using caosdb::entity::v1alpha1::FileUploadResponse;
    using caosdb::entity::v1alpha1::IdResponse;
    using caosdb::entity::v1alpha1::MultiTransactionRequest;
    using caosdb::entity::v1alpha1::MultiTransactionResponse;
    using caosdb::entity::v1alpha1::RegisterFileDownloadRequest;
    using caosdb::entity::v1alpha1::RegisterFileDownloadResponse;
    using caosdb::entity::v1alpha1::RegisterFileUploadResponse;
    using caosdb::transaction::TransactionStatus;
    using WrappedResponseCase =
      caosdb::entity::v1alpha1::TransactionResponse::WrappedResponseCase;
    
    class Transaction;
    
    static const std::string logger_name = "caosdb::transaction";
    
    /**
     * Abstract base class for the results of a Transaction.
     */
    class ResultSet {
      class iterator;
    
    public:
      virtual ~ResultSet() = default;
      [[nodiscard]] virtual auto Size() const noexcept -> int = 0;
      [[nodiscard]] virtual auto At(const int index) const -> const Entity & = 0;
      auto begin() const -> iterator;
      auto end() const -> iterator;
    
    private:
      class iterator : public std::iterator<std::output_iterator_tag, Entity> {
      public:
        explicit iterator(const ResultSet *result_set, int index = 0);
        auto operator*() const -> const Entity &;
        auto operator++() -> iterator &;
        auto operator++(int) -> iterator;
        auto operator!=(const iterator &rhs) const -> bool;
    
      private:
        int current_index = 0;
        const ResultSet *result_set;
      };
    };
    
    /**
     * Container with results of a transaction.
     *
     * In contrast to UniqueResult, this one can also hold multiple entities or zero
     * entities.
     */
    class MultiResultSet : public ResultSet {
    public:
      ~MultiResultSet() = default;
      explicit MultiResultSet(std::vector<std::unique_ptr<Entity>> result_set);
      [[nodiscard]] inline auto Size() const noexcept -> int override {
        return this->entities.size();
      }
      [[nodiscard]] inline auto At(const int index) const
        -> const Entity & override {
        return *(this->entities.at(index));
      }
      std::vector<std::unique_ptr<Entity>> entities;
    };
    
    /**
     * Container with the single result of a transaction.
     *
     * In contrast to MultiResultSet, this one guarantees to hold exactly one
     * entity.
     */
    class UniqueResult : public ResultSet {
    public:
      ~UniqueResult() = default;
      explicit inline UniqueResult(ProtoEntity *protoEntity)
        : entity(new Entity(protoEntity)){};
      explicit inline UniqueResult(IdResponse *idResponse)
        : entity(new Entity(idResponse)){};
      [[nodiscard]] auto GetEntity() const -> const Entity &;
      [[nodiscard]] inline auto Size() const noexcept -> int override { return 1; }
      [[nodiscard]] inline auto At(const int index) const
        -> const Entity & override {
        if (index != 0) {
          throw std::out_of_range("Index out of range. Length is 1.");
        }
        return *(this->entity);
      }
    
    private:
      std::unique_ptr<Entity> entity;
    };
    
    // TODO(fspreck) One possibility for count-queries: Transaction gets a
    // GetQueryCount function that returns exactly this. In case of a
    // single FIND Query this will be equivalent to the Size of the
    // ResultSet, but it may be different in case of Query + RetrieveById,
    // and in case of COUNT queries which have an empty ResultSet. We have
    // to think about how to extend this to transactions with more than
    // one query in the future. My suggestion is to treat a transaction
    // with more than one query as the union of the query results in which
    // case the query count would return the cumulative number of
    // results. This would even allow the combination of FIND, FIND
    // unique, and COUNT, albeit a little strangely.
    /**
     * @brief Create a transaction via `CaosDBConnection.createTransaction()`
     */
    class Transaction {
    public:
      auto UploadFile(FileUploadResponse *response,
                      const std::string &registration_id) -> void;
      auto RegisterUploadFile(RegisterFileUploadResponse *response) -> void;
      auto DownloadFile(FileDownloadResponse *response,
                        const RegisterFileDownloadResponse &registration_response)
        -> void;
      auto RegisterDownloadFile(const RegisterFileDownloadRequest &request,
                                RegisterFileDownloadResponse *response) -> void;
      /**
       * The transaction type restricts the kind of sub-transaction which may be
       * added to a transaction (insertion, update, deletion, retrieval).
       *
       * @note MIXED_READ_AND_WRITE and MIXED_WRITE transaction are not supported
       * yet.
       */
      enum TransactionType {
        NONE,                //!< Unspecified or not specified yet.
        READ_ONLY,           //!< Only retrievals (by id, by query)
        INSERT,              //!< Only insertions
        UPDATE,              //!< Only updates
        DELETE,              //!< Only deletions
        MIXED_WRITE,         //!< Only insertions, deletions, updates
        MIXED_READ_AND_WRITE //!< all kind of transaction.
      };
      Transaction(std::shared_ptr<EntityTransactionService::Stub> entity_service,
                  std::shared_ptr<FileTransmissionService::Stub> file_service);
    
      /**
       * Add an entity id to this transaction for retrieval.
       *
       * The retrieval is being processed when the Execute() or
       * ExecuteAsynchronously() methods of this transaction are called.
       */
      auto RetrieveById(const std::string &id) noexcept -> StatusCode;
    
      /**
       * Add all entity ids to this transaction for retrieval.
       */
      template <class InputIterator>
      inline auto RetrieveById(InputIterator begin, InputIterator end) noexcept
        -> StatusCode;
    
      /**
       * Add a query to this transaction.
       *
       * Currently, only FIND and COUNT queries are supported.
       */
      auto Query(const std::string &query) noexcept -> StatusCode;
    
      /**
       * Add the entity to this transaction for an insertion.
       *
       * The insertion is being processed when the Execute() or
       * ExecuteAsynchronously() methods of this transaction are called.
       *
       * Changing the entity afterwards results in undefined behavior.
       */
      auto InsertEntity(Entity *entity) noexcept -> StatusCode;
    
      /**
       * Add the entity to this transaction for an update.
       *
       * The update is being processed when the Execute() or
       * ExecuteAsynchronously() methods of this transaction are called.
       *
       * Changing the entity afterwards results in undefined behavior.
       */
      auto UpdateEntity(Entity *entity) noexcept -> StatusCode;
    
      /**
       * Add an entity id to this transaction for deletion.
       *
       * The deletion is being processed when the Execute() or
       * ExecuteAsynchronously() methods of this transaction are called.
       */
      auto DeleteById(const std::string &id) noexcept -> StatusCode;
    
      inline auto IsStatus(const TransactionStatus &status) const noexcept -> bool {
        return this->status.GetCode() == status.GetCode();
      };
    
      /**
       * Execute this transaction in blocking mode and return the status.
       */
      auto Execute() -> TransactionStatus;
    
      /**
       * Execute this transaction in non-blocking mode and return immediately.
       *
       * A client may request the current status at any time via GetStatus().
       *
       * Use WaitForIt() to join the back-ground execution of this transaction.
       */
      auto ExecuteAsynchronously() noexcept -> StatusCode;
    
      /**
       * Join the background execution and return the status when the execution
       * terminates.
       *
       * Use this after ExecuteAsynchronously().
       */
      [[nodiscard]] auto WaitForIt() const noexcept -> TransactionStatus;
    
      /**
       * Return the current status of the transaction.
       */
      [[nodiscard]] inline auto GetStatus() const -> TransactionStatus {
        return this->status;
      }
    
      [[nodiscard]] inline auto GetResultSet() const -> const ResultSet & {
        const ResultSet *result_set = this->result_set.get();
        return *result_set;
      }
    
      /**
       * Return the result of a count query
       *
       * Only meaningful if there was exactly one COUNT query executed in
       * this transaction. In all other cases, the return value will be
       * -1.
       */
      [[nodiscard]] inline auto GetCountResult() const -> int {
        return query_count;
      }
    
      /**
       * Return the number of sub-requests in this transaction.
       *
       * This is meant for debugging because the number of sub-requests is a
       * GRPC-API detail.
       */
      [[nodiscard]] inline auto GetRequestCount() const -> int {
        return this->request->requests_size();
      }
    
      /**
       * Get a JSON representation of the respone.
       *
       * For debugging.
       */
      inline auto ResponseToString() const -> const std::string {
        google::protobuf::util::JsonOptions options;
        std::string out;
        google::protobuf::util::MessageToJsonString(*this->response, &out, options);
        return out;
      }
    
      /**
       * Get a JSON representation of the request.
       *
       * For debugging.
       */
      inline auto RequestToString() const -> const std::string {
        google::protobuf::util::JsonOptions options;
        std::string out;
        google::protobuf::util::MessageToJsonString(*this->request, &out, options);
        return out;
      }
    
    private:
      bool has_query = false;
      TransactionType transaction_type = TransactionType::NONE;
      mutable std::unique_ptr<ResultSet> result_set;
      mutable TransactionStatus status = TransactionStatus::INITIAL();
      std::shared_ptr<EntityTransactionService::Stub> entity_service;
      std::shared_ptr<FileTransmissionService::Stub> file_service;
      MultiTransactionRequest *request;
      mutable MultiTransactionResponse *response;
      std::string error_message;
      mutable long query_count;
    };
    
    template <class InputIterator>
    inline auto Transaction::RetrieveById(InputIterator begin,
                                          InputIterator end) noexcept
      -> StatusCode {
      ASSERT_CAN_ADD_RETRIEVAL
    
      auto next = begin;
      while (next != end) {
        auto *sub_request = this->request->add_requests();
        sub_request->mutable_retrieve_request()->set_id(*next);
        next = std::next(next);
      }
    
      this->status = TransactionStatus::GO_ON();
      return this->status.GetCode();
    }
    
    } // namespace caosdb::transaction
    #endif