547 lines
23 KiB
C++
547 lines
23 KiB
C++
// Copyright 2024 Memgraph Ltd.
|
|
//
|
|
// Use of this software is governed by the Business Source License
|
|
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
|
// License, and you may not use this file except in compliance with the Business Source License.
|
|
//
|
|
// As of the Change Date specified in that file, in accordance with
|
|
// the Business Source License, use of this software will be governed
|
|
// by the Apache License, Version 2.0, included in the file
|
|
// licenses/APL.txt.
|
|
|
|
#include <thread>
|
|
#include "absl/container/flat_hash_set.h"
|
|
#include "spdlog/spdlog.h"
|
|
|
|
#include "storage/v2/disk/name_id_mapper.hpp"
|
|
#include "storage/v2/storage.hpp"
|
|
#include "storage/v2/transaction.hpp"
|
|
#include "storage/v2/vertex_accessor.hpp"
|
|
#include "utils/atomic_memory_block.hpp"
|
|
#include "utils/event_counter.hpp"
|
|
#include "utils/event_histogram.hpp"
|
|
#include "utils/exceptions.hpp"
|
|
#include "utils/file.hpp"
|
|
#include "utils/logging.hpp"
|
|
#include "utils/stat.hpp"
|
|
#include "utils/timer.hpp"
|
|
#include "utils/typeinfo.hpp"
|
|
#include "utils/uuid.hpp"
|
|
|
|
namespace memgraph::storage {
|
|
|
|
class InMemoryStorage;
|
|
|
|
Storage::Storage(Config config, StorageMode storage_mode)
|
|
: name_id_mapper_(std::invoke([config, storage_mode]() -> std::unique_ptr<NameIdMapper> {
|
|
if (storage_mode == StorageMode::ON_DISK_TRANSACTIONAL) {
|
|
return std::make_unique<DiskNameIdMapper>(config.disk.name_id_mapper_directory,
|
|
config.disk.id_name_mapper_directory);
|
|
}
|
|
return std::make_unique<NameIdMapper>();
|
|
})),
|
|
config_(config),
|
|
isolation_level_(config.transaction.isolation_level),
|
|
storage_mode_(storage_mode),
|
|
indices_(config, storage_mode),
|
|
constraints_(config, storage_mode) {
|
|
spdlog::info("Created database with {} storage mode.", StorageModeToString(storage_mode));
|
|
}
|
|
|
|
Storage::Accessor::Accessor(SharedAccess /* tag */, Storage *storage, IsolationLevel isolation_level,
|
|
StorageMode storage_mode,
|
|
memgraph::replication_coordination_glue::ReplicationRole replication_role)
|
|
: storage_(storage),
|
|
// The lock must be acquired before creating the transaction object to
|
|
// prevent freshly created transactions from dangling in an active state
|
|
// during exclusive operations.
|
|
storage_guard_(storage_->main_lock_),
|
|
unique_guard_(storage_->main_lock_, std::defer_lock),
|
|
transaction_(storage->CreateTransaction(isolation_level, storage_mode, replication_role)),
|
|
is_transaction_active_(true),
|
|
creation_storage_mode_(storage_mode) {}
|
|
|
|
Storage::Accessor::Accessor(UniqueAccess /* tag */, Storage *storage, IsolationLevel isolation_level,
|
|
StorageMode storage_mode,
|
|
memgraph::replication_coordination_glue::ReplicationRole replication_role)
|
|
: storage_(storage),
|
|
// The lock must be acquired before creating the transaction object to
|
|
// prevent freshly created transactions from dangling in an active state
|
|
// during exclusive operations.
|
|
storage_guard_(storage_->main_lock_, std::defer_lock),
|
|
unique_guard_(storage_->main_lock_),
|
|
transaction_(storage->CreateTransaction(isolation_level, storage_mode, replication_role)),
|
|
is_transaction_active_(true),
|
|
creation_storage_mode_(storage_mode) {}
|
|
|
|
Storage::Accessor::Accessor(Accessor &&other) noexcept
|
|
: storage_(other.storage_),
|
|
storage_guard_(std::move(other.storage_guard_)),
|
|
unique_guard_(std::move(other.unique_guard_)),
|
|
transaction_(std::move(other.transaction_)),
|
|
commit_timestamp_(other.commit_timestamp_),
|
|
is_transaction_active_(other.is_transaction_active_),
|
|
creation_storage_mode_(other.creation_storage_mode_) {
|
|
// Don't allow the other accessor to abort our transaction in destructor.
|
|
other.is_transaction_active_ = false;
|
|
other.commit_timestamp_.reset();
|
|
}
|
|
|
|
StorageMode Storage::GetStorageMode() const noexcept { return storage_mode_; }
|
|
|
|
IsolationLevel Storage::GetIsolationLevel() const noexcept { return isolation_level_; }
|
|
|
|
utils::BasicResult<Storage::SetIsolationLevelError> Storage::SetIsolationLevel(IsolationLevel isolation_level) {
|
|
std::unique_lock main_guard{main_lock_};
|
|
isolation_level_ = isolation_level;
|
|
return {};
|
|
}
|
|
|
|
StorageMode Storage::Accessor::GetCreationStorageMode() const noexcept { return creation_storage_mode_; }
|
|
|
|
std::optional<uint64_t> Storage::Accessor::GetTransactionId() const {
|
|
if (is_transaction_active_) {
|
|
return transaction_.transaction_id;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::vector<LabelId> Storage::Accessor::ListAllPossiblyPresentVertexLabels() const {
|
|
std::vector<LabelId> vertex_labels;
|
|
storage_->stored_node_labels_.for_each([&vertex_labels](const auto &label) { vertex_labels.push_back(label); });
|
|
return vertex_labels;
|
|
}
|
|
|
|
std::vector<EdgeTypeId> Storage::Accessor::ListAllPossiblyPresentEdgeTypes() const {
|
|
std::vector<EdgeTypeId> edge_types;
|
|
storage_->stored_edge_types_.for_each([&edge_types](const auto &type) { edge_types.push_back(type); });
|
|
return edge_types;
|
|
}
|
|
|
|
void Storage::Accessor::AdvanceCommand() {
|
|
transaction_.manyDeltasCache.Clear(); // TODO: Just invalidate the View::OLD cache, NEW should still be fine
|
|
++transaction_.command_id;
|
|
}
|
|
|
|
Result<std::optional<VertexAccessor>> Storage::Accessor::DeleteVertex(VertexAccessor *vertex) {
|
|
/// NOTE: Checking whether the vertex can be deleted must be done by loading edges from disk.
|
|
/// Loading edges is done through VertexAccessor so we do it here.
|
|
if (storage_->storage_mode_ == StorageMode::ON_DISK_TRANSACTIONAL) {
|
|
auto out_edges_res = vertex->OutEdges(View::OLD);
|
|
auto in_edges_res = vertex->InEdges(View::OLD);
|
|
if (out_edges_res.HasError() && out_edges_res.GetError() != Error::NONEXISTENT_OBJECT) {
|
|
return out_edges_res.GetError();
|
|
}
|
|
if (!out_edges_res.HasError() && !out_edges_res->edges.empty()) {
|
|
return Error::VERTEX_HAS_EDGES;
|
|
}
|
|
if (in_edges_res.HasError() && in_edges_res.GetError() != Error::NONEXISTENT_OBJECT) {
|
|
return in_edges_res.GetError();
|
|
}
|
|
if (!in_edges_res.HasError() && !in_edges_res->edges.empty()) {
|
|
return Error::VERTEX_HAS_EDGES;
|
|
}
|
|
}
|
|
auto res = DetachDelete({vertex}, {}, false);
|
|
|
|
if (res.HasError()) {
|
|
return res.GetError();
|
|
}
|
|
|
|
const auto &value = res.GetValue();
|
|
if (!value) {
|
|
return std::optional<VertexAccessor>{};
|
|
}
|
|
|
|
const auto &[vertices, edges] = *value;
|
|
|
|
MG_ASSERT(vertices.size() <= 1, "The number of deleted vertices is not less or equal to 1!");
|
|
MG_ASSERT(edges.empty(), "Deleting a vertex without detaching should not have resulted in deleting any edges!");
|
|
|
|
if (vertices.empty()) {
|
|
return std::optional<VertexAccessor>{};
|
|
}
|
|
|
|
return std::make_optional<VertexAccessor>(vertices[0]);
|
|
}
|
|
|
|
Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> Storage::Accessor::DetachDeleteVertex(
|
|
VertexAccessor *vertex) {
|
|
using ReturnType = std::pair<VertexAccessor, std::vector<EdgeAccessor>>;
|
|
if (storage_->storage_mode_ == StorageMode::ON_DISK_TRANSACTIONAL) {
|
|
auto out_edges_res = vertex->OutEdges(View::OLD);
|
|
auto in_edges_res = vertex->InEdges(View::OLD);
|
|
if (out_edges_res.HasError() && out_edges_res.GetError() != Error::NONEXISTENT_OBJECT) {
|
|
return out_edges_res.GetError();
|
|
}
|
|
if (in_edges_res.HasError() && in_edges_res.GetError() != Error::NONEXISTENT_OBJECT) {
|
|
return in_edges_res.GetError();
|
|
}
|
|
}
|
|
|
|
auto res = DetachDelete({vertex}, {}, true);
|
|
|
|
if (res.HasError()) {
|
|
return res.GetError();
|
|
}
|
|
|
|
auto &value = res.GetValue();
|
|
if (!value) {
|
|
return std::optional<ReturnType>{};
|
|
}
|
|
|
|
auto &[vertices, edges] = *value;
|
|
|
|
MG_ASSERT(vertices.size() <= 1, "The number of detach deleted vertices is not less or equal to 1!");
|
|
|
|
return std::make_optional<ReturnType>(vertices[0], std::move(edges));
|
|
}
|
|
|
|
Result<std::optional<EdgeAccessor>> Storage::Accessor::DeleteEdge(EdgeAccessor *edge) {
|
|
auto res = DetachDelete({}, {edge}, false);
|
|
|
|
if (res.HasError()) {
|
|
return res.GetError();
|
|
}
|
|
|
|
const auto &value = res.GetValue();
|
|
if (!value) {
|
|
return std::optional<EdgeAccessor>{};
|
|
}
|
|
|
|
const auto &[vertices, edges] = *value;
|
|
|
|
MG_ASSERT(vertices.empty(), "Deleting an edge should not have deleted a vertex!");
|
|
MG_ASSERT(edges.size() <= 1, "Deleted edges need to be less or equal to 1!");
|
|
|
|
if (edges.empty()) {
|
|
return std::optional<EdgeAccessor>{};
|
|
}
|
|
|
|
return std::make_optional<EdgeAccessor>(edges[0]);
|
|
}
|
|
|
|
Result<std::optional<std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>>>
|
|
Storage::Accessor::DetachDelete(std::vector<VertexAccessor *> nodes, std::vector<EdgeAccessor *> edges, bool detach) {
|
|
using ReturnType = std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>;
|
|
if (storage_->storage_mode_ == StorageMode::ON_DISK_TRANSACTIONAL) {
|
|
for (const auto *vertex : nodes) {
|
|
/// TODO: (andi) Extract into a separate function.
|
|
auto out_edges_res = vertex->OutEdges(View::OLD);
|
|
auto in_edges_res = vertex->InEdges(View::OLD);
|
|
if (out_edges_res.HasError() && out_edges_res.GetError() != Error::NONEXISTENT_OBJECT) {
|
|
return out_edges_res.GetError();
|
|
}
|
|
if (in_edges_res.HasError() && in_edges_res.GetError() != Error::NONEXISTENT_OBJECT) {
|
|
return in_edges_res.GetError();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1. Gather nodes which are not deleted yet in the system
|
|
auto maybe_nodes_to_delete = PrepareDeletableNodes(nodes);
|
|
if (maybe_nodes_to_delete.HasError()) {
|
|
return maybe_nodes_to_delete.GetError();
|
|
}
|
|
const std::unordered_set<Vertex *> nodes_to_delete = *maybe_nodes_to_delete.GetValue();
|
|
|
|
// 2. Gather edges and corresponding node on the other end of the edge for the deletable nodes
|
|
EdgeInfoForDeletion edge_deletion_info = PrepareDeletableEdges(nodes_to_delete, edges, detach);
|
|
|
|
// Detach nodes which need to be deleted
|
|
std::unordered_set<Gid> deleted_edge_ids;
|
|
std::vector<EdgeAccessor> deleted_edges;
|
|
if (detach) {
|
|
auto maybe_cleared_edges = ClearEdgesOnVertices(nodes_to_delete, deleted_edge_ids);
|
|
if (maybe_cleared_edges.HasError()) {
|
|
return maybe_cleared_edges.GetError();
|
|
}
|
|
|
|
deleted_edges = *maybe_cleared_edges.GetValue();
|
|
}
|
|
|
|
// Detach nodes on the other end, which don't need deletion, by passing once through their vectors
|
|
auto maybe_remaining_edges = DetachRemainingEdges(std::move(edge_deletion_info), deleted_edge_ids);
|
|
if (maybe_remaining_edges.HasError()) {
|
|
return maybe_remaining_edges.GetError();
|
|
}
|
|
const std::vector<EdgeAccessor> remaining_edges = *maybe_remaining_edges.GetValue();
|
|
deleted_edges.insert(deleted_edges.end(), remaining_edges.begin(), remaining_edges.end());
|
|
|
|
auto const maybe_deleted_vertices = TryDeleteVertices(nodes_to_delete);
|
|
if (maybe_deleted_vertices.HasError()) {
|
|
return maybe_deleted_vertices.GetError();
|
|
}
|
|
|
|
auto deleted_vertices = maybe_deleted_vertices.GetValue();
|
|
|
|
return std::make_optional<ReturnType>(std::move(deleted_vertices), std::move(deleted_edges));
|
|
}
|
|
|
|
Result<std::optional<std::unordered_set<Vertex *>>> Storage::Accessor::PrepareDeletableNodes(
|
|
const std::vector<VertexAccessor *> &vertices) {
|
|
// Some of the vertices could be already deleted in the system so we need to check
|
|
std::unordered_set<Vertex *> nodes_to_delete{};
|
|
for (const auto &vertex : vertices) {
|
|
MG_ASSERT(vertex->transaction_ == &transaction_,
|
|
"VertexAccessor must be from the same transaction as the storage "
|
|
"accessor when deleting a vertex!");
|
|
auto *vertex_ptr = vertex->vertex_;
|
|
/// TODO: (andi) This is overhead for disk storage, we should not lock the vertex here
|
|
{
|
|
auto vertex_lock = std::unique_lock{vertex_ptr->lock};
|
|
|
|
if (!PrepareForWrite(&transaction_, vertex_ptr)) return Error::SERIALIZATION_ERROR;
|
|
|
|
if (vertex_ptr->deleted) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
nodes_to_delete.insert(vertex_ptr);
|
|
}
|
|
|
|
return std::make_optional<std::unordered_set<Vertex *>>(nodes_to_delete);
|
|
}
|
|
|
|
EdgeInfoForDeletion Storage::Accessor::PrepareDeletableEdges(const std::unordered_set<Vertex *> &vertices,
|
|
const std::vector<EdgeAccessor *> &edges,
|
|
bool detach) noexcept {
|
|
std::unordered_set<Vertex *> partial_src_vertices;
|
|
std::unordered_set<Vertex *> partial_dest_vertices;
|
|
std::unordered_set<Gid> src_edge_ids;
|
|
std::unordered_set<Gid> dest_edge_ids;
|
|
|
|
auto try_adding_partial_delete_vertices = [this, &vertices](auto &partial_delete_vertices, auto &edge_ids,
|
|
auto &item) {
|
|
// For the nodes on the other end of the edge, they might not get deleted in the system but only cut out
|
|
// of the edge. Therefore, information is gathered in this step to account for every vertices' in and out
|
|
// edges and what must be deleted
|
|
const auto &[edge_type, opposing_vertex, edge] = item;
|
|
if (!vertices.contains(opposing_vertex)) {
|
|
partial_delete_vertices.insert(opposing_vertex);
|
|
auto const edge_gid = storage_->config_.salient.items.properties_on_edges ? edge.ptr->gid : edge.gid;
|
|
edge_ids.insert(edge_gid);
|
|
}
|
|
};
|
|
|
|
// add nodes which need to be detached on the other end of the edge
|
|
if (detach) {
|
|
for (auto *vertex_ptr : vertices) {
|
|
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> in_edges;
|
|
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> out_edges;
|
|
|
|
{
|
|
auto vertex_lock = std::shared_lock{vertex_ptr->lock};
|
|
in_edges = vertex_ptr->in_edges;
|
|
out_edges = vertex_ptr->out_edges;
|
|
}
|
|
|
|
for (auto const &item : in_edges) {
|
|
try_adding_partial_delete_vertices(partial_src_vertices, src_edge_ids, item);
|
|
}
|
|
for (auto const &item : out_edges) {
|
|
try_adding_partial_delete_vertices(partial_dest_vertices, dest_edge_ids, item);
|
|
}
|
|
}
|
|
}
|
|
|
|
// also add edges which we want to delete from the query
|
|
for (const auto &edge_accessor : edges) {
|
|
if (edge_accessor->from_vertex_->deleted || edge_accessor->to_vertex_->deleted) {
|
|
continue;
|
|
}
|
|
partial_src_vertices.insert(edge_accessor->from_vertex_);
|
|
partial_dest_vertices.insert(edge_accessor->to_vertex_);
|
|
|
|
auto const edge_gid = edge_accessor->Gid();
|
|
src_edge_ids.insert(edge_gid);
|
|
dest_edge_ids.insert(edge_gid);
|
|
}
|
|
|
|
return EdgeInfoForDeletion{.partial_src_edge_ids = std::move(src_edge_ids),
|
|
.partial_dest_edge_ids = std::move(dest_edge_ids),
|
|
.partial_src_vertices = std::move(partial_src_vertices),
|
|
.partial_dest_vertices = std::move(partial_dest_vertices)};
|
|
}
|
|
|
|
Result<std::optional<std::vector<EdgeAccessor>>> Storage::Accessor::ClearEdgesOnVertices(
|
|
const std::unordered_set<Vertex *> &vertices, std::unordered_set<Gid> &deleted_edge_ids) {
|
|
// We want to gather all edges that we delete in this step so that we can proceed with
|
|
// further deletion
|
|
using ReturnType = std::vector<EdgeAccessor>;
|
|
std::vector<EdgeAccessor> deleted_edges{};
|
|
|
|
auto clear_edges = [this, &deleted_edges, &deleted_edge_ids](
|
|
auto *vertex_ptr, auto *attached_edges_to_vertex, auto deletion_delta,
|
|
auto reverse_vertex_order) -> Result<std::optional<ReturnType>> {
|
|
auto vertex_lock = std::unique_lock{vertex_ptr->lock};
|
|
while (!attached_edges_to_vertex->empty()) {
|
|
// get the information about the last edge in the vertex collection
|
|
auto const &[edge_type, opposing_vertex, edge_ref] = *attached_edges_to_vertex->rbegin();
|
|
|
|
/// TODO: (andi) Again here, no need to lock the edge if using on disk storage.
|
|
std::unique_lock<utils::RWSpinLock> guard;
|
|
if (storage_->config_.salient.items.properties_on_edges) {
|
|
auto edge_ptr = edge_ref.ptr;
|
|
guard = std::unique_lock{edge_ptr->lock};
|
|
|
|
if (!PrepareForWrite(&transaction_, edge_ptr)) return Error::SERIALIZATION_ERROR;
|
|
}
|
|
|
|
if (!PrepareForWrite(&transaction_, vertex_ptr)) return Error::SERIALIZATION_ERROR;
|
|
MG_ASSERT(!vertex_ptr->deleted, "Invalid database state!");
|
|
|
|
// MarkEdgeAsDeleted allocates additional memory
|
|
// and CreateAndLinkDelta needs memory
|
|
utils::AtomicMemoryBlock atomic_memory_block{[&attached_edges_to_vertex, &deleted_edge_ids, &reverse_vertex_order,
|
|
&vertex_ptr, &deleted_edges, deletion_delta = deletion_delta,
|
|
edge_type = edge_type, opposing_vertex = opposing_vertex,
|
|
edge_ref = edge_ref, this]() {
|
|
attached_edges_to_vertex->pop_back();
|
|
if (this->storage_->config_.salient.items.properties_on_edges) {
|
|
auto *edge_ptr = edge_ref.ptr;
|
|
MarkEdgeAsDeleted(edge_ptr);
|
|
}
|
|
|
|
auto const edge_gid = storage_->config_.salient.items.properties_on_edges ? edge_ref.ptr->gid : edge_ref.gid;
|
|
auto const [_, was_inserted] = deleted_edge_ids.insert(edge_gid);
|
|
bool const edge_cleared_from_both_directions = !was_inserted;
|
|
if (edge_cleared_from_both_directions) {
|
|
auto *from_vertex = reverse_vertex_order ? vertex_ptr : opposing_vertex;
|
|
auto *to_vertex = reverse_vertex_order ? opposing_vertex : vertex_ptr;
|
|
deleted_edges.emplace_back(edge_ref, edge_type, from_vertex, to_vertex, storage_, &transaction_, true);
|
|
}
|
|
CreateAndLinkDelta(&transaction_, vertex_ptr, deletion_delta, edge_type, opposing_vertex, edge_ref);
|
|
}};
|
|
std::invoke(atomic_memory_block);
|
|
}
|
|
|
|
return std::make_optional<ReturnType>();
|
|
};
|
|
|
|
// delete the in and out edges from the nodes we want to delete
|
|
// no need to lock here, we are just passing the pointer of the in and out edges collections
|
|
for (auto *vertex_ptr : vertices) {
|
|
auto maybe_error = clear_edges(vertex_ptr, &vertex_ptr->in_edges, Delta::AddInEdgeTag(), false);
|
|
if (maybe_error.HasError()) {
|
|
return maybe_error;
|
|
}
|
|
|
|
maybe_error = clear_edges(vertex_ptr, &vertex_ptr->out_edges, Delta::AddOutEdgeTag(), true);
|
|
if (maybe_error.HasError()) {
|
|
return maybe_error;
|
|
}
|
|
}
|
|
|
|
return std::make_optional<ReturnType>(deleted_edges);
|
|
}
|
|
|
|
Result<std::optional<std::vector<EdgeAccessor>>> Storage::Accessor::DetachRemainingEdges(
|
|
EdgeInfoForDeletion info, std::unordered_set<Gid> &partially_detached_edge_ids) {
|
|
using ReturnType = std::vector<EdgeAccessor>;
|
|
std::vector<EdgeAccessor> deleted_edges{};
|
|
|
|
auto clear_edges_on_other_direction = [this, &deleted_edges, &partially_detached_edge_ids](
|
|
auto *vertex_ptr, auto *edges_attached_to_vertex, auto &set_for_erasure,
|
|
auto deletion_delta,
|
|
auto reverse_vertex_order) -> Result<std::optional<ReturnType>> {
|
|
auto vertex_lock = std::unique_lock{vertex_ptr->lock};
|
|
|
|
if (!PrepareForWrite(&transaction_, vertex_ptr)) return Error::SERIALIZATION_ERROR;
|
|
MG_ASSERT(!vertex_ptr->deleted, "Invalid database state!");
|
|
|
|
auto mid = std::partition(
|
|
edges_attached_to_vertex->begin(), edges_attached_to_vertex->end(), [this, &set_for_erasure](auto &edge) {
|
|
auto const &[edge_type, opposing_vertex, edge_ref] = edge;
|
|
auto const edge_gid = storage_->config_.salient.items.properties_on_edges ? edge_ref.ptr->gid : edge_ref.gid;
|
|
return !set_for_erasure.contains(edge_gid);
|
|
});
|
|
|
|
// Creating deltas and erasing edge only at the end -> we might have incomplete state as
|
|
// delta might cause OOM, so we don't remove edges from edges_attached_to_vertex
|
|
utils::AtomicMemoryBlock atomic_memory_block{[&mid, &edges_attached_to_vertex, &deleted_edges,
|
|
&partially_detached_edge_ids, this, vertex_ptr, deletion_delta,
|
|
reverse_vertex_order]() {
|
|
for (auto it = mid; it != edges_attached_to_vertex->end(); it++) {
|
|
auto const &[edge_type, opposing_vertex, edge_ref] = *it;
|
|
std::unique_lock<utils::RWSpinLock> guard;
|
|
if (storage_->config_.salient.items.properties_on_edges) {
|
|
auto edge_ptr = edge_ref.ptr;
|
|
guard = std::unique_lock{edge_ptr->lock};
|
|
// this can happen only if we marked edges for deletion with no nodes,
|
|
// so the method detaching nodes will not do anything
|
|
MarkEdgeAsDeleted(edge_ptr);
|
|
}
|
|
|
|
CreateAndLinkDelta(&transaction_, vertex_ptr, deletion_delta, edge_type, opposing_vertex, edge_ref);
|
|
|
|
auto const edge_gid = storage_->config_.salient.items.properties_on_edges ? edge_ref.ptr->gid : edge_ref.gid;
|
|
auto const [_, was_inserted] = partially_detached_edge_ids.insert(edge_gid);
|
|
bool const edge_cleared_from_both_directions = !was_inserted;
|
|
if (edge_cleared_from_both_directions) {
|
|
auto *from_vertex = reverse_vertex_order ? opposing_vertex : vertex_ptr;
|
|
auto *to_vertex = reverse_vertex_order ? vertex_ptr : opposing_vertex;
|
|
deleted_edges.emplace_back(edge_ref, edge_type, from_vertex, to_vertex, storage_, &transaction_, true);
|
|
}
|
|
}
|
|
edges_attached_to_vertex->erase(mid, edges_attached_to_vertex->end());
|
|
}};
|
|
|
|
std::invoke(atomic_memory_block);
|
|
return std::make_optional<ReturnType>();
|
|
};
|
|
|
|
// remove edges from vertex collections which we aggregated for just detaching
|
|
for (auto *vertex_ptr : info.partial_src_vertices) {
|
|
auto maybe_error = clear_edges_on_other_direction(vertex_ptr, &vertex_ptr->out_edges, info.partial_src_edge_ids,
|
|
Delta::AddOutEdgeTag(), false);
|
|
if (maybe_error.HasError()) {
|
|
return maybe_error;
|
|
}
|
|
}
|
|
for (auto *vertex_ptr : info.partial_dest_vertices) {
|
|
auto maybe_error = clear_edges_on_other_direction(vertex_ptr, &vertex_ptr->in_edges, info.partial_dest_edge_ids,
|
|
Delta::AddInEdgeTag(), true);
|
|
if (maybe_error.HasError()) {
|
|
return maybe_error;
|
|
}
|
|
}
|
|
|
|
return std::make_optional<ReturnType>(deleted_edges);
|
|
}
|
|
|
|
Result<std::vector<VertexAccessor>> Storage::Accessor::TryDeleteVertices(const std::unordered_set<Vertex *> &vertices) {
|
|
std::vector<VertexAccessor> deleted_vertices;
|
|
deleted_vertices.reserve(vertices.size());
|
|
|
|
for (auto *vertex_ptr : vertices) {
|
|
auto vertex_lock = std::unique_lock{vertex_ptr->lock};
|
|
|
|
if (!PrepareForWrite(&transaction_, vertex_ptr)) return Error::SERIALIZATION_ERROR;
|
|
|
|
MG_ASSERT(!vertex_ptr->deleted, "Invalid database state!");
|
|
|
|
if (!vertex_ptr->in_edges.empty() || !vertex_ptr->out_edges.empty()) {
|
|
return Error::VERTEX_HAS_EDGES;
|
|
}
|
|
|
|
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
|
vertex_ptr->deleted = true;
|
|
|
|
deleted_vertices.emplace_back(vertex_ptr, storage_, &transaction_, true);
|
|
}
|
|
|
|
return deleted_vertices;
|
|
}
|
|
|
|
void Storage::Accessor::MarkEdgeAsDeleted(Edge *edge) {
|
|
if (!edge->deleted) {
|
|
CreateAndLinkDelta(&transaction_, edge, Delta::RecreateObjectTag());
|
|
edge->deleted = true;
|
|
storage_->edge_count_.fetch_sub(1, std::memory_order_acq_rel);
|
|
}
|
|
}
|
|
|
|
} // namespace memgraph::storage
|