Add detach delete in bulk (#1078)
This commit is contained in:
parent
ab56abf4ca
commit
58546a9fe1
4
.github/workflows/diff.yaml
vendored
4
.github/workflows/diff.yaml
vendored
@ -101,7 +101,7 @@ jobs:
|
|||||||
echo ${file}
|
echo ${file}
|
||||||
if [[ ${file} == *.py ]]; then
|
if [[ ${file} == *.py ]]; then
|
||||||
python3 -m black --check --diff ${file}
|
python3 -m black --check --diff ${file}
|
||||||
python3 -m isort --check-only --diff ${file}
|
python3 -m isort --profile black --check-only --diff ${file}
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
@ -275,11 +275,13 @@ jobs:
|
|||||||
- name: Run stress test (plain)
|
- name: Run stress test (plain)
|
||||||
run: |
|
run: |
|
||||||
cd tests/stress
|
cd tests/stress
|
||||||
|
source ve3/bin/activate
|
||||||
./continuous_integration
|
./continuous_integration
|
||||||
|
|
||||||
- name: Run stress test (SSL)
|
- name: Run stress test (SSL)
|
||||||
run: |
|
run: |
|
||||||
cd tests/stress
|
cd tests/stress
|
||||||
|
source ve3/bin/activate
|
||||||
./continuous_integration --use-ssl
|
./continuous_integration --use-ssl
|
||||||
|
|
||||||
- name: Run durability test
|
- name: Run durability test
|
||||||
|
@ -15,6 +15,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
|
args: ["--profile", "black"]
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||||
rev: v13.0.0
|
rev: v13.0.0
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
#include <cppitertools/filter.hpp>
|
#include <cppitertools/filter.hpp>
|
||||||
#include <cppitertools/imap.hpp>
|
#include <cppitertools/imap.hpp>
|
||||||
@ -469,8 +470,8 @@ class DbAccessor final {
|
|||||||
|
|
||||||
std::vector<EdgeAccessor> deleted_edges;
|
std::vector<EdgeAccessor> deleted_edges;
|
||||||
deleted_edges.reserve(edges.size());
|
deleted_edges.reserve(edges.size());
|
||||||
std::transform(edges.begin(), edges.end(), std::back_inserter(deleted_edges),
|
std::ranges::transform(edges, std::back_inserter(deleted_edges),
|
||||||
[](const auto &deleted_edge) { return EdgeAccessor{deleted_edge}; });
|
[](const auto &deleted_edge) { return EdgeAccessor{deleted_edge}; });
|
||||||
|
|
||||||
return std::make_optional<ReturnType>(vertex, std::move(deleted_edges));
|
return std::make_optional<ReturnType>(vertex, std::move(deleted_edges));
|
||||||
}
|
}
|
||||||
@ -489,6 +490,53 @@ class DbAccessor final {
|
|||||||
return std::make_optional<VertexAccessor>(*value);
|
return std::make_optional<VertexAccessor>(*value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storage::Result<std::optional<std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>>> DetachDelete(
|
||||||
|
std::vector<VertexAccessor> nodes, std::vector<EdgeAccessor> edges, bool detach) {
|
||||||
|
using ReturnType = std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>;
|
||||||
|
|
||||||
|
std::vector<storage::VertexAccessor *> nodes_impl;
|
||||||
|
std::vector<storage::EdgeAccessor *> edges_impl;
|
||||||
|
|
||||||
|
nodes_impl.reserve(nodes.size());
|
||||||
|
edges_impl.reserve(edges.size());
|
||||||
|
|
||||||
|
for (auto &vertex_accessor : nodes) {
|
||||||
|
accessor_->PrefetchOutEdges(vertex_accessor.impl_);
|
||||||
|
accessor_->PrefetchInEdges(vertex_accessor.impl_);
|
||||||
|
|
||||||
|
nodes_impl.push_back(&vertex_accessor.impl_);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &edge_accessor : edges) {
|
||||||
|
edges_impl.push_back(&edge_accessor.impl_);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto res = accessor_->DetachDelete(std::move(nodes_impl), std::move(edges_impl), detach);
|
||||||
|
if (res.HasError()) {
|
||||||
|
return res.GetError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &value = res.GetValue();
|
||||||
|
if (!value) {
|
||||||
|
return std::optional<ReturnType>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &[val_vertices, val_edges] = *value;
|
||||||
|
|
||||||
|
std::vector<VertexAccessor> deleted_vertices;
|
||||||
|
std::vector<EdgeAccessor> deleted_edges;
|
||||||
|
|
||||||
|
deleted_vertices.reserve(val_vertices.size());
|
||||||
|
deleted_edges.reserve(val_edges.size());
|
||||||
|
|
||||||
|
std::ranges::transform(val_vertices, std::back_inserter(deleted_vertices),
|
||||||
|
[](const auto &deleted_vertex) { return VertexAccessor{deleted_vertex}; });
|
||||||
|
std::ranges::transform(val_edges, std::back_inserter(deleted_edges),
|
||||||
|
[](const auto &deleted_edge) { return EdgeAccessor{deleted_edge}; });
|
||||||
|
|
||||||
|
return std::make_optional<ReturnType>(std::move(deleted_vertices), std::move(deleted_edges));
|
||||||
|
}
|
||||||
|
|
||||||
storage::PropertyId NameToProperty(const std::string_view name) { return accessor_->NameToProperty(name); }
|
storage::PropertyId NameToProperty(const std::string_view name) { return accessor_->NameToProperty(name); }
|
||||||
|
|
||||||
storage::LabelId NameToLabel(const std::string_view name) { return accessor_->NameToLabel(name); }
|
storage::LabelId NameToLabel(const std::string_view name) { return accessor_->NameToLabel(name); }
|
||||||
|
@ -2560,15 +2560,12 @@ std::vector<Symbol> Delete::ModifiedSymbols(const SymbolTable &table) const { re
|
|||||||
Delete::DeleteCursor::DeleteCursor(const Delete &self, utils::MemoryResource *mem)
|
Delete::DeleteCursor::DeleteCursor(const Delete &self, utils::MemoryResource *mem)
|
||||||
: self_(self), input_cursor_(self_.input_->MakeCursor(mem)) {}
|
: self_(self), input_cursor_(self_.input_->MakeCursor(mem)) {}
|
||||||
|
|
||||||
bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
void Delete::DeleteCursor::UpdateDeleteBuffer(Frame &frame, ExecutionContext &context) {
|
||||||
SCOPED_PROFILE_OP("Delete");
|
|
||||||
|
|
||||||
if (!input_cursor_->Pull(frame, context)) return false;
|
|
||||||
|
|
||||||
// Delete should get the latest information, this way it is also possible
|
// Delete should get the latest information, this way it is also possible
|
||||||
// to delete newly added nodes and edges.
|
// to delete newly added nodes and edges.
|
||||||
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
|
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
|
||||||
storage::View::NEW);
|
storage::View::NEW);
|
||||||
|
|
||||||
auto *pull_memory = context.evaluation_context.memory;
|
auto *pull_memory = context.evaluation_context.memory;
|
||||||
// collect expressions results so edges can get deleted before vertices
|
// collect expressions results so edges can get deleted before vertices
|
||||||
// this is necessary because an edge that gets deleted could block vertex
|
// this is necessary because an edge that gets deleted could block vertex
|
||||||
@ -2579,107 +2576,34 @@ bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
|||||||
expression_results.emplace_back(expression->Accept(evaluator));
|
expression_results.emplace_back(expression->Accept(evaluator));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &dba = *context.db_accessor;
|
|
||||||
// delete edges first
|
|
||||||
for (TypedValue &expression_result : expression_results) {
|
|
||||||
AbortCheck(context);
|
|
||||||
if (expression_result.type() == TypedValue::Type::Edge) {
|
|
||||||
auto &ea = expression_result.ValueEdge();
|
|
||||||
#ifdef MG_ENTERPRISE
|
|
||||||
if (license::global_license_checker.IsEnterpriseValidFast() && context.auth_checker &&
|
|
||||||
!(context.auth_checker->Has(ea, query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE) &&
|
|
||||||
context.auth_checker->Has(ea.To(), storage::View::NEW, query::AuthQuery::FineGrainedPrivilege::UPDATE) &&
|
|
||||||
context.auth_checker->Has(ea.From(), storage::View::NEW, query::AuthQuery::FineGrainedPrivilege::UPDATE))) {
|
|
||||||
throw QueryRuntimeException("Edge not deleted due to not having enough permission!");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
auto maybe_value = dba.RemoveEdge(&ea);
|
|
||||||
if (maybe_value.HasError()) {
|
|
||||||
switch (maybe_value.GetError()) {
|
|
||||||
case storage::Error::SERIALIZATION_ERROR:
|
|
||||||
throw TransactionSerializationException();
|
|
||||||
case storage::Error::DELETED_OBJECT:
|
|
||||||
case storage::Error::VERTEX_HAS_EDGES:
|
|
||||||
case storage::Error::PROPERTIES_DISABLED:
|
|
||||||
case storage::Error::NONEXISTENT_OBJECT:
|
|
||||||
throw QueryRuntimeException("Unexpected error when deleting an edge.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.execution_stats[ExecutionStats::Key::DELETED_EDGES] += 1;
|
|
||||||
if (context.trigger_context_collector && maybe_value.GetValue()) {
|
|
||||||
context.trigger_context_collector->RegisterDeletedObject(*maybe_value.GetValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete vertices
|
|
||||||
for (TypedValue &expression_result : expression_results) {
|
for (TypedValue &expression_result : expression_results) {
|
||||||
AbortCheck(context);
|
AbortCheck(context);
|
||||||
switch (expression_result.type()) {
|
switch (expression_result.type()) {
|
||||||
case TypedValue::Type::Vertex: {
|
case TypedValue::Type::Vertex: {
|
||||||
auto &va = expression_result.ValueVertex();
|
auto va = expression_result.ValueVertex();
|
||||||
#ifdef MG_ENTERPRISE
|
#ifdef MG_ENTERPRISE
|
||||||
if (license::global_license_checker.IsEnterpriseValidFast() && context.auth_checker &&
|
if (license::global_license_checker.IsEnterpriseValidFast() && context.auth_checker &&
|
||||||
!context.auth_checker->Has(va, storage::View::NEW, query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) {
|
!context.auth_checker->Has(va, storage::View::NEW, query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) {
|
||||||
throw QueryRuntimeException("Vertex not deleted due to not having enough permission!");
|
throw QueryRuntimeException("Vertex not deleted due to not having enough permission!");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (self_.detach_) {
|
buffer_.nodes.push_back(va);
|
||||||
auto res = dba.DetachRemoveVertex(&va);
|
break;
|
||||||
if (res.HasError()) {
|
}
|
||||||
switch (res.GetError()) {
|
case TypedValue::Type::Edge: {
|
||||||
case storage::Error::SERIALIZATION_ERROR:
|
auto ea = expression_result.ValueEdge();
|
||||||
throw TransactionSerializationException();
|
#ifdef MG_ENTERPRISE
|
||||||
case storage::Error::DELETED_OBJECT:
|
if (license::global_license_checker.IsEnterpriseValidFast() && context.auth_checker &&
|
||||||
case storage::Error::VERTEX_HAS_EDGES:
|
!(context.auth_checker->Has(ea, query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE) &&
|
||||||
case storage::Error::PROPERTIES_DISABLED:
|
context.auth_checker->Has(ea.To(), storage::View::NEW, query::AuthQuery::FineGrainedPrivilege::UPDATE) &&
|
||||||
case storage::Error::NONEXISTENT_OBJECT:
|
context.auth_checker->Has(ea.From(), storage::View::NEW,
|
||||||
throw QueryRuntimeException("Unexpected error when deleting a node.");
|
query::AuthQuery::FineGrainedPrivilege::UPDATE))) {
|
||||||
}
|
throw QueryRuntimeException("Edge not deleted due to not having enough permission!");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
context.execution_stats[ExecutionStats::Key::DELETED_NODES] += 1;
|
buffer_.edges.push_back(ea);
|
||||||
if (*res) {
|
|
||||||
context.execution_stats[ExecutionStats::Key::DELETED_EDGES] += static_cast<int64_t>((*res)->second.size());
|
|
||||||
}
|
|
||||||
std::invoke([&] {
|
|
||||||
if (!context.trigger_context_collector || !*res) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.trigger_context_collector->RegisterDeletedObject((*res)->first);
|
|
||||||
if (!context.trigger_context_collector->ShouldRegisterDeletedObject<query::EdgeAccessor>()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const auto &edge : (*res)->second) {
|
|
||||||
context.trigger_context_collector->RegisterDeletedObject(edge);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
auto res = dba.RemoveVertex(&va);
|
|
||||||
if (res.HasError()) {
|
|
||||||
switch (res.GetError()) {
|
|
||||||
case storage::Error::SERIALIZATION_ERROR:
|
|
||||||
throw TransactionSerializationException();
|
|
||||||
case storage::Error::VERTEX_HAS_EDGES:
|
|
||||||
throw RemoveAttachedVertexException();
|
|
||||||
case storage::Error::DELETED_OBJECT:
|
|
||||||
case storage::Error::PROPERTIES_DISABLED:
|
|
||||||
case storage::Error::NONEXISTENT_OBJECT:
|
|
||||||
throw QueryRuntimeException("Unexpected error when deleting a node.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.execution_stats[ExecutionStats::Key::DELETED_NODES] += 1;
|
|
||||||
if (context.trigger_context_collector && res.GetValue()) {
|
|
||||||
context.trigger_context_collector->RegisterDeletedObject(*res.GetValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip Edges (already deleted) and Nulls (can occur in optional
|
|
||||||
// match)
|
|
||||||
case TypedValue::Type::Edge:
|
|
||||||
case TypedValue::Type::Null:
|
case TypedValue::Type::Null:
|
||||||
break;
|
break;
|
||||||
// check we're not trying to delete anything except vertices and edges
|
// check we're not trying to delete anything except vertices and edges
|
||||||
@ -2687,13 +2611,64 @@ bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
|||||||
throw QueryRuntimeException("Only edges and vertices can be deleted.");
|
throw QueryRuntimeException("Only edges and vertices can be deleted.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||||
|
SCOPED_PROFILE_OP("Delete");
|
||||||
|
|
||||||
|
if (delete_executed_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input_cursor_->Pull(frame, context)) {
|
||||||
|
UpdateDeleteBuffer(frame, context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &dba = *context.db_accessor;
|
||||||
|
auto res = dba.DetachDelete(std::move(buffer_.nodes), std::move(buffer_.edges), self_.detach_);
|
||||||
|
if (res.HasError()) {
|
||||||
|
switch (res.GetError()) {
|
||||||
|
case storage::Error::SERIALIZATION_ERROR:
|
||||||
|
throw TransactionSerializationException();
|
||||||
|
case storage::Error::VERTEX_HAS_EDGES:
|
||||||
|
throw RemoveAttachedVertexException();
|
||||||
|
case storage::Error::DELETED_OBJECT:
|
||||||
|
case storage::Error::PROPERTIES_DISABLED:
|
||||||
|
case storage::Error::NONEXISTENT_OBJECT:
|
||||||
|
throw QueryRuntimeException("Unexpected error when deleting a node.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*res) {
|
||||||
|
context.execution_stats[ExecutionStats::Key::DELETED_NODES] += static_cast<int64_t>((*res)->first.size());
|
||||||
|
context.execution_stats[ExecutionStats::Key::DELETED_EDGES] += static_cast<int64_t>((*res)->second.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update deleted objects for triggers
|
||||||
|
if (context.trigger_context_collector && *res) {
|
||||||
|
for (const auto &node : (*res)->first) {
|
||||||
|
context.trigger_context_collector->RegisterDeletedObject(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.trigger_context_collector->ShouldRegisterDeletedObject<query::EdgeAccessor>()) {
|
||||||
|
for (const auto &edge : (*res)->second) {
|
||||||
|
context.trigger_context_collector->RegisterDeletedObject(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_executed_ = true;
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Delete::DeleteCursor::Shutdown() { input_cursor_->Shutdown(); }
|
void Delete::DeleteCursor::Shutdown() { input_cursor_->Shutdown(); }
|
||||||
|
|
||||||
void Delete::DeleteCursor::Reset() { input_cursor_->Reset(); }
|
void Delete::DeleteCursor::Reset() {
|
||||||
|
input_cursor_->Reset();
|
||||||
|
delete_executed_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
SetProperty::SetProperty(const std::shared_ptr<LogicalOperator> &input, storage::PropertyId property,
|
SetProperty::SetProperty(const std::shared_ptr<LogicalOperator> &input, storage::PropertyId property,
|
||||||
PropertyLookup *lhs, Expression *rhs)
|
PropertyLookup *lhs, Expression *rhs)
|
||||||
@ -4953,7 +4928,9 @@ class LoadCsvCursor : public Cursor {
|
|||||||
if (input_cursor_->Pull(frame, context)) {
|
if (input_cursor_->Pull(frame, context)) {
|
||||||
if (did_pull_) {
|
if (did_pull_) {
|
||||||
throw QueryRuntimeException(
|
throw QueryRuntimeException(
|
||||||
"LOAD CSV can be executed only once, please check if the cardinality of the operator before LOAD CSV is 1");
|
"LOAD CSV can be executed only once, please check if the cardinality of the operator before LOAD CSV "
|
||||||
|
"is "
|
||||||
|
"1");
|
||||||
}
|
}
|
||||||
did_pull_ = true;
|
did_pull_ = true;
|
||||||
}
|
}
|
||||||
|
@ -1121,6 +1121,11 @@ class Produce : public memgraph::query::plan::LogicalOperator {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct DeleteBuffer {
|
||||||
|
std::vector<VertexAccessor> nodes{};
|
||||||
|
std::vector<EdgeAccessor> edges{};
|
||||||
|
};
|
||||||
|
|
||||||
/// Operator for deleting vertices and edges.
|
/// Operator for deleting vertices and edges.
|
||||||
///
|
///
|
||||||
/// Has a flag for using DETACH DELETE when deleting vertices.
|
/// Has a flag for using DETACH DELETE when deleting vertices.
|
||||||
@ -1168,6 +1173,10 @@ class Delete : public memgraph::query::plan::LogicalOperator {
|
|||||||
private:
|
private:
|
||||||
const Delete &self_;
|
const Delete &self_;
|
||||||
const UniqueCursorPtr input_cursor_;
|
const UniqueCursorPtr input_cursor_;
|
||||||
|
DeleteBuffer buffer_;
|
||||||
|
bool delete_executed_{false};
|
||||||
|
|
||||||
|
void UpdateDeleteBuffer(Frame &, ExecutionContext &);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1085,79 +1085,44 @@ std::optional<VertexAccessor> DiskStorage::DiskAccessor::FindVertex(storage::Gid
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<std::optional<VertexAccessor>> DiskStorage::DiskAccessor::DeleteVertex(VertexAccessor *vertex) {
|
Result<std::optional<std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>>>
|
||||||
auto *vertex_ptr = vertex->vertex_;
|
DiskStorage::DiskAccessor::DetachDelete(std::vector<VertexAccessor *> nodes, std::vector<EdgeAccessor *> edges,
|
||||||
|
bool detach) {
|
||||||
|
using ReturnType = std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>;
|
||||||
|
|
||||||
if (vertex_ptr->deleted) {
|
auto maybe_result = Storage::Accessor::DetachDelete(nodes, edges, detach);
|
||||||
return std::optional<VertexAccessor>{};
|
|
||||||
|
if (maybe_result.HasError()) {
|
||||||
|
return maybe_result.GetError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vertex_ptr->in_edges.empty() || !vertex_ptr->out_edges.empty()) return Error::VERTEX_HAS_EDGES;
|
auto value = maybe_result.GetValue();
|
||||||
|
|
||||||
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
if (!value) {
|
||||||
vertex_ptr->deleted = true;
|
return std::make_optional<ReturnType>();
|
||||||
vertices_to_delete_.emplace_back(utils::SerializeIdType(vertex_ptr->gid), utils::SerializeVertex(*vertex_ptr));
|
}
|
||||||
transaction_.manyDeltasCache.Invalidate(vertex_ptr);
|
|
||||||
|
auto &[deleted_vertices, deleted_edges] = *value;
|
||||||
|
|
||||||
auto *disk_storage = static_cast<DiskStorage *>(storage_);
|
auto *disk_storage = static_cast<DiskStorage *>(storage_);
|
||||||
disk_storage->vertex_count_.fetch_sub(1, std::memory_order_acq_rel);
|
for (const auto &vertex : deleted_vertices) {
|
||||||
|
vertices_to_delete_.emplace_back(utils::SerializeIdType(vertex.vertex_->gid),
|
||||||
return std::make_optional<VertexAccessor>(vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_,
|
utils::SerializeVertex(*vertex.vertex_));
|
||||||
config_, true);
|
transaction_.manyDeltasCache.Invalidate(vertex.vertex_);
|
||||||
}
|
disk_storage->vertex_count_.fetch_sub(1, std::memory_order_acq_rel);
|
||||||
|
|
||||||
Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>>
|
|
||||||
DiskStorage::DiskAccessor::DetachDeleteVertex(VertexAccessor *vertex) {
|
|
||||||
using ReturnType = std::pair<VertexAccessor, std::vector<EdgeAccessor>>;
|
|
||||||
auto *vertex_ptr = vertex->vertex_;
|
|
||||||
|
|
||||||
if (vertex_ptr->deleted) return std::optional<ReturnType>{};
|
|
||||||
|
|
||||||
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> in_edges{vertex_ptr->in_edges};
|
|
||||||
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> out_edges{vertex_ptr->out_edges};
|
|
||||||
|
|
||||||
std::vector<EdgeAccessor> deleted_edges;
|
|
||||||
for (const auto &item : in_edges) {
|
|
||||||
auto [edge_type, from_vertex, edge] = item;
|
|
||||||
EdgeAccessor e(edge, edge_type, from_vertex, vertex_ptr, &transaction_, &storage_->indices_,
|
|
||||||
&storage_->constraints_, config_);
|
|
||||||
auto ret = DeleteEdge(&e);
|
|
||||||
if (ret.HasError()) {
|
|
||||||
MG_ASSERT(ret.GetError() == Error::SERIALIZATION_ERROR, "Invalid database state!");
|
|
||||||
return ret.GetError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ret.GetValue()) {
|
|
||||||
deleted_edges.push_back(*ret.GetValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto &item : out_edges) {
|
|
||||||
auto [edge_type, to_vertex, edge] = item;
|
|
||||||
EdgeAccessor e(edge, edge_type, vertex_ptr, to_vertex, &transaction_, &storage_->indices_, &storage_->constraints_,
|
|
||||||
config_);
|
|
||||||
auto ret = DeleteEdge(&e);
|
|
||||||
if (ret.HasError()) {
|
|
||||||
MG_ASSERT(ret.GetError() == Error::SERIALIZATION_ERROR, "Invalid database state!");
|
|
||||||
return ret.GetError();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ret.GetValue()) {
|
|
||||||
deleted_edges.push_back(*ret.GetValue());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MG_ASSERT(!vertex_ptr->deleted, "Invalid database state!");
|
for (const auto &edge : deleted_edges) {
|
||||||
|
const DiskEdgeKey disk_edge_key(edge.from_vertex_->gid, edge.to_vertex_->gid, edge.edge_type_, edge.edge_,
|
||||||
|
config_.properties_on_edges);
|
||||||
|
edges_to_delete_.emplace(disk_edge_key.GetSerializedKey());
|
||||||
|
|
||||||
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
transaction_.manyDeltasCache.Invalidate(edge.from_vertex_, edge.edge_type_, EdgeDirection::OUT);
|
||||||
vertex_ptr->deleted = true;
|
transaction_.manyDeltasCache.Invalidate(edge.to_vertex_, edge.edge_type_, EdgeDirection::IN);
|
||||||
vertices_to_delete_.emplace_back(utils::SerializeIdType(vertex_ptr->gid), utils::SerializeVertex(*vertex_ptr));
|
transaction_.RemoveModifiedEdge(edge.Gid());
|
||||||
auto *disk_storage = static_cast<DiskStorage *>(storage_);
|
}
|
||||||
disk_storage->vertex_count_.fetch_sub(1, std::memory_order_acq_rel);
|
|
||||||
transaction_.manyDeltasCache.Invalidate(vertex_ptr);
|
|
||||||
|
|
||||||
return std::make_optional<ReturnType>(
|
return maybe_result;
|
||||||
VertexAccessor{vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_, config_, true},
|
|
||||||
std::move(deleted_edges));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DiskStorage::DiskAccessor::PrefetchEdgeFilter(const std::string_view disk_edge_key_str,
|
bool DiskStorage::DiskAccessor::PrefetchEdgeFilter(const std::string_view disk_edge_key_str,
|
||||||
@ -1300,73 +1265,6 @@ Result<EdgeAccessor> DiskStorage::DiskAccessor::CreateEdge(VertexAccessor *from,
|
|||||||
&storage_->constraints_, config_);
|
&storage_->constraints_, config_);
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<std::optional<EdgeAccessor>> DiskStorage::DiskAccessor::DeleteEdge(EdgeAccessor *edge) {
|
|
||||||
MG_ASSERT(edge->transaction_ == &transaction_,
|
|
||||||
"EdgeAccessor must be from the same transaction as the storage "
|
|
||||||
"accessor when deleting an edge!");
|
|
||||||
const auto edge_ref = edge->edge_;
|
|
||||||
const auto edge_type = edge->edge_type_;
|
|
||||||
|
|
||||||
if (config_.properties_on_edges && edge_ref.ptr->deleted) return std::optional<EdgeAccessor>{};
|
|
||||||
|
|
||||||
auto *from_vertex = edge->from_vertex_;
|
|
||||||
auto *to_vertex = edge->to_vertex_;
|
|
||||||
|
|
||||||
MG_ASSERT(!from_vertex->deleted, "Invalid database state!");
|
|
||||||
|
|
||||||
if (to_vertex != from_vertex) {
|
|
||||||
MG_ASSERT(!to_vertex->deleted, "Invalid database state!");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto delete_edge_from_storage = [&edge_type, &edge_ref, this](auto *vertex, auto *edges) {
|
|
||||||
const std::tuple<EdgeTypeId, Vertex *, EdgeRef> link(edge_type, vertex, edge_ref);
|
|
||||||
auto it = std::find(edges->begin(), edges->end(), link);
|
|
||||||
if (config_.properties_on_edges) {
|
|
||||||
MG_ASSERT(it != edges->end(), "Invalid database state!");
|
|
||||||
} else if (it == edges->end()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::swap(*it, *edges->rbegin());
|
|
||||||
edges->pop_back();
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto op1 = delete_edge_from_storage(to_vertex, &from_vertex->out_edges);
|
|
||||||
const auto op2 = delete_edge_from_storage(from_vertex, &to_vertex->in_edges);
|
|
||||||
|
|
||||||
const DiskEdgeKey disk_edge_key(from_vertex->gid, to_vertex->gid, edge_type, edge_ref, config_.properties_on_edges);
|
|
||||||
edges_to_delete_.emplace(disk_edge_key.GetSerializedKey());
|
|
||||||
|
|
||||||
transaction_.RemoveModifiedEdge(edge->Gid());
|
|
||||||
|
|
||||||
if (config_.properties_on_edges) {
|
|
||||||
MG_ASSERT((op1 && op2), "Invalid database state!");
|
|
||||||
} else {
|
|
||||||
MG_ASSERT((op1 && op2) || (!op1 && !op2), "Invalid database state!");
|
|
||||||
if (!op1 && !op2) {
|
|
||||||
// The edge is already deleted.
|
|
||||||
return std::optional<EdgeAccessor>{};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config_.properties_on_edges) {
|
|
||||||
auto *edge_ptr = edge_ref.ptr;
|
|
||||||
CreateAndLinkDelta(&transaction_, edge_ptr, Delta::RecreateObjectTag());
|
|
||||||
edge_ptr->deleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateAndLinkDelta(&transaction_, from_vertex, Delta::AddOutEdgeTag(), edge_type, to_vertex, edge_ref);
|
|
||||||
CreateAndLinkDelta(&transaction_, to_vertex, Delta::AddInEdgeTag(), edge_type, from_vertex, edge_ref);
|
|
||||||
|
|
||||||
transaction_.manyDeltasCache.Invalidate(from_vertex, edge_type, EdgeDirection::OUT);
|
|
||||||
transaction_.manyDeltasCache.Invalidate(to_vertex, edge_type, EdgeDirection::IN);
|
|
||||||
|
|
||||||
storage_->edge_count_.fetch_sub(1, std::memory_order_acq_rel);
|
|
||||||
|
|
||||||
return std::make_optional<EdgeAccessor>(edge_ref, edge_type, from_vertex, to_vertex, &transaction_,
|
|
||||||
&storage_->indices_, &storage_->constraints_, config_, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<EdgeAccessor> DiskStorage::DiskAccessor::EdgeSetFrom(EdgeAccessor * /*edge*/, VertexAccessor * /*new_from*/) {
|
Result<EdgeAccessor> DiskStorage::DiskAccessor::EdgeSetFrom(EdgeAccessor * /*edge*/, VertexAccessor * /*new_from*/) {
|
||||||
MG_ASSERT(false, "EdgeSetFrom is currently only implemented for InMemory storage");
|
MG_ASSERT(false, "EdgeSetFrom is currently only implemented for InMemory storage");
|
||||||
return Error::NONEXISTENT_OBJECT;
|
return Error::NONEXISTENT_OBJECT;
|
||||||
|
@ -172,11 +172,8 @@ class DiskStorage final : public Storage {
|
|||||||
throw utils::NotYetImplemented("SetIndexStats(stats) is not implemented for DiskStorage.");
|
throw utils::NotYetImplemented("SetIndexStats(stats) is not implemented for DiskStorage.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TODO: It is just marked as deleted but the memory isn't reclaimed because of the in-memory storage
|
Result<std::optional<std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>>> DetachDelete(
|
||||||
Result<std::optional<VertexAccessor>> DeleteVertex(VertexAccessor *vertex) override;
|
std::vector<VertexAccessor *> nodes, std::vector<EdgeAccessor *> edges, bool detach) override;
|
||||||
|
|
||||||
Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> DetachDeleteVertex(
|
|
||||||
VertexAccessor *vertex) override;
|
|
||||||
|
|
||||||
void PrefetchInEdges(const VertexAccessor &vertex_acc) override;
|
void PrefetchInEdges(const VertexAccessor &vertex_acc) override;
|
||||||
|
|
||||||
@ -188,8 +185,6 @@ class DiskStorage final : public Storage {
|
|||||||
|
|
||||||
Result<EdgeAccessor> EdgeSetTo(EdgeAccessor *edge, VertexAccessor *new_to) override;
|
Result<EdgeAccessor> EdgeSetTo(EdgeAccessor *edge, VertexAccessor *new_to) override;
|
||||||
|
|
||||||
Result<std::optional<EdgeAccessor>> DeleteEdge(EdgeAccessor *edge) override;
|
|
||||||
|
|
||||||
bool LabelIndexExists(LabelId label) const override {
|
bool LabelIndexExists(LabelId label) const override {
|
||||||
auto *disk_storage = static_cast<DiskStorage *>(storage_);
|
auto *disk_storage = static_cast<DiskStorage *>(storage_);
|
||||||
return disk_storage->indices_.label_index_->IndexExists(label);
|
return disk_storage->indices_.label_index_->IndexExists(label);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2022 Memgraph Ltd.
|
// Copyright 2023 Memgraph Ltd.
|
||||||
//
|
//
|
||||||
// Use of this software is governed by the Business Source License
|
// 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
|
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||||
@ -36,7 +36,6 @@ static_assert(std::is_standard_layout_v<Edge *>, "The Edge * must have a standar
|
|||||||
static_assert(std::is_standard_layout_v<EdgeRef>, "The EdgeRef must have a standard layout!");
|
static_assert(std::is_standard_layout_v<EdgeRef>, "The EdgeRef must have a standard layout!");
|
||||||
|
|
||||||
inline bool operator==(const EdgeRef &a, const EdgeRef &b) noexcept { return a.gid == b.gid; }
|
inline bool operator==(const EdgeRef &a, const EdgeRef &b) noexcept { return a.gid == b.gid; }
|
||||||
|
inline bool operator<(const EdgeRef &first, const EdgeRef &second) { return first.gid < second.gid; }
|
||||||
inline bool operator!=(const EdgeRef &a, const EdgeRef &b) noexcept { return a.gid != b.gid; }
|
inline bool operator!=(const EdgeRef &a, const EdgeRef &b) noexcept { return a.gid != b.gid; }
|
||||||
|
|
||||||
} // namespace memgraph::storage
|
} // namespace memgraph::storage
|
||||||
|
@ -245,114 +245,51 @@ std::optional<VertexAccessor> InMemoryStorage::InMemoryAccessor::FindVertex(Gid
|
|||||||
return VertexAccessor::Create(&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_, view);
|
return VertexAccessor::Create(&*it, &transaction_, &storage_->indices_, &storage_->constraints_, config_, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<std::optional<VertexAccessor>> InMemoryStorage::InMemoryAccessor::DeleteVertex(VertexAccessor *vertex) {
|
Result<std::optional<std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>>>
|
||||||
MG_ASSERT(vertex->transaction_ == &transaction_,
|
InMemoryStorage::InMemoryAccessor::DetachDelete(std::vector<VertexAccessor *> nodes, std::vector<EdgeAccessor *> edges,
|
||||||
"VertexAccessor must be from the same transaction as the storage "
|
bool detach) {
|
||||||
"accessor when deleting a vertex!");
|
using ReturnType = std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>;
|
||||||
auto *vertex_ptr = vertex->vertex_;
|
|
||||||
|
|
||||||
auto guard = std::unique_lock{vertex_ptr->lock};
|
auto maybe_result = Storage::Accessor::DetachDelete(nodes, edges, detach);
|
||||||
|
|
||||||
if (!PrepareForWrite(&transaction_, vertex_ptr)) return Error::SERIALIZATION_ERROR;
|
if (maybe_result.HasError()) {
|
||||||
|
return maybe_result.GetError();
|
||||||
if (vertex_ptr->deleted) {
|
|
||||||
return std::optional<VertexAccessor>{};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vertex_ptr->in_edges.empty() || !vertex_ptr->out_edges.empty()) return Error::VERTEX_HAS_EDGES;
|
auto value = maybe_result.GetValue();
|
||||||
|
|
||||||
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
if (!value) {
|
||||||
vertex_ptr->deleted = true;
|
return std::make_optional<ReturnType>();
|
||||||
transaction_.manyDeltasCache.Invalidate(vertex_ptr);
|
}
|
||||||
|
|
||||||
|
auto &[deleted_vertices, deleted_edges] = *value;
|
||||||
|
|
||||||
// Need to inform the next CollectGarbage call that there are some
|
// Need to inform the next CollectGarbage call that there are some
|
||||||
// non-transactional deletions that need to be collected
|
// non-transactional deletions that need to be collected
|
||||||
if (transaction_.storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
|
auto const inform_gc_vertex_deletion = utils::OnScopeExit{[this, &deleted_vertices = deleted_vertices]() {
|
||||||
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
|
if (!deleted_vertices.empty() && transaction_.storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
|
||||||
mem_storage->gc_full_scan_vertices_delete_ = true;
|
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
|
||||||
}
|
mem_storage->gc_full_scan_vertices_delete_ = true;
|
||||||
|
|
||||||
return std::make_optional<VertexAccessor>(vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_,
|
|
||||||
config_, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>>
|
|
||||||
InMemoryStorage::InMemoryAccessor::DetachDeleteVertex(VertexAccessor *vertex) {
|
|
||||||
using ReturnType = std::pair<VertexAccessor, std::vector<EdgeAccessor>>;
|
|
||||||
|
|
||||||
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_;
|
|
||||||
|
|
||||||
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> in_edges;
|
|
||||||
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> out_edges;
|
|
||||||
|
|
||||||
{
|
|
||||||
auto guard = std::unique_lock{vertex_ptr->lock};
|
|
||||||
|
|
||||||
if (!PrepareForWrite(&transaction_, vertex_ptr)) return Error::SERIALIZATION_ERROR;
|
|
||||||
|
|
||||||
if (vertex_ptr->deleted) return std::optional<ReturnType>{};
|
|
||||||
|
|
||||||
in_edges = vertex_ptr->in_edges;
|
|
||||||
out_edges = vertex_ptr->out_edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<EdgeAccessor> deleted_edges;
|
|
||||||
for (const auto &item : in_edges) {
|
|
||||||
auto [edge_type, from_vertex, edge] = item;
|
|
||||||
EdgeAccessor e(edge, edge_type, from_vertex, vertex_ptr, &transaction_, &storage_->indices_,
|
|
||||||
&storage_->constraints_, config_);
|
|
||||||
auto ret = DeleteEdge(&e);
|
|
||||||
if (ret.HasError()) {
|
|
||||||
MG_ASSERT(ret.GetError() == Error::SERIALIZATION_ERROR, "Invalid database state!");
|
|
||||||
return ret.GetError();
|
|
||||||
}
|
}
|
||||||
|
}};
|
||||||
|
|
||||||
if (ret.GetValue()) {
|
auto const inform_gc_edge_deletion = utils::OnScopeExit{[this, &deleted_edges = deleted_edges]() {
|
||||||
deleted_edges.push_back(*ret.GetValue());
|
if (!deleted_edges.empty() && transaction_.storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
|
||||||
}
|
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
|
||||||
}
|
mem_storage->gc_full_scan_edges_delete_ = true;
|
||||||
for (const auto &item : out_edges) {
|
|
||||||
auto [edge_type, to_vertex, edge] = item;
|
|
||||||
EdgeAccessor e(edge, edge_type, vertex_ptr, to_vertex, &transaction_, &storage_->indices_, &storage_->constraints_,
|
|
||||||
config_);
|
|
||||||
auto ret = DeleteEdge(&e);
|
|
||||||
if (ret.HasError()) {
|
|
||||||
MG_ASSERT(ret.GetError() == Error::SERIALIZATION_ERROR, "Invalid database state!");
|
|
||||||
return ret.GetError();
|
|
||||||
}
|
}
|
||||||
|
}};
|
||||||
|
|
||||||
if (ret.GetValue()) {
|
for (auto const &vertex : deleted_vertices) {
|
||||||
deleted_edges.push_back(*ret.GetValue());
|
transaction_.manyDeltasCache.Invalidate(vertex.vertex_);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto guard = std::unique_lock{vertex_ptr->lock};
|
for (const auto &edge : deleted_edges) {
|
||||||
|
transaction_.manyDeltasCache.Invalidate(edge.from_vertex_, edge.edge_type_, EdgeDirection::OUT);
|
||||||
// We need to check again for serialization errors because we unlocked the
|
transaction_.manyDeltasCache.Invalidate(edge.to_vertex_, edge.edge_type_, EdgeDirection::IN);
|
||||||
// vertex. Some other transaction could have modified the vertex in the
|
|
||||||
// meantime if we didn't have any edges to delete.
|
|
||||||
|
|
||||||
if (!PrepareForWrite(&transaction_, vertex_ptr)) return Error::SERIALIZATION_ERROR;
|
|
||||||
|
|
||||||
MG_ASSERT(!vertex_ptr->deleted, "Invalid database state!");
|
|
||||||
|
|
||||||
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
|
||||||
vertex_ptr->deleted = true;
|
|
||||||
transaction_.manyDeltasCache.Invalidate(vertex_ptr);
|
|
||||||
|
|
||||||
// Need to inform the next CollectGarbage call that there are some
|
|
||||||
// non-transactional deletions that need to be collected
|
|
||||||
if (transaction_.storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
|
|
||||||
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
|
|
||||||
mem_storage->gc_full_scan_vertices_delete_ = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::make_optional<ReturnType>(
|
return maybe_result;
|
||||||
VertexAccessor{vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_, config_, true},
|
|
||||||
std::move(deleted_edges));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::CreateEdge(VertexAccessor *from, VertexAccessor *to,
|
Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::CreateEdge(VertexAccessor *from, VertexAccessor *to,
|
||||||
@ -700,100 +637,6 @@ Result<EdgeAccessor> InMemoryStorage::InMemoryAccessor::EdgeSetTo(EdgeAccessor *
|
|||||||
&storage_->constraints_, config_);
|
&storage_->constraints_, config_);
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<std::optional<EdgeAccessor>> InMemoryStorage::InMemoryAccessor::DeleteEdge(EdgeAccessor *edge) {
|
|
||||||
MG_ASSERT(edge->transaction_ == &transaction_,
|
|
||||||
"EdgeAccessor must be from the same transaction as the storage "
|
|
||||||
"accessor when deleting an edge!");
|
|
||||||
auto &edge_ref = edge->edge_;
|
|
||||||
auto &edge_type = edge->edge_type_;
|
|
||||||
|
|
||||||
std::unique_lock<utils::RWSpinLock> guard;
|
|
||||||
if (config_.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 (edge_ptr->deleted) return std::optional<EdgeAccessor>{};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *from_vertex = edge->from_vertex_;
|
|
||||||
auto *to_vertex = edge->to_vertex_;
|
|
||||||
|
|
||||||
// Obtain the locks by `gid` order to avoid lock cycles.
|
|
||||||
auto guard_from = std::unique_lock{from_vertex->lock, std::defer_lock};
|
|
||||||
auto guard_to = std::unique_lock{to_vertex->lock, std::defer_lock};
|
|
||||||
if (from_vertex->gid < to_vertex->gid) {
|
|
||||||
guard_from.lock();
|
|
||||||
guard_to.lock();
|
|
||||||
} else if (from_vertex->gid > to_vertex->gid) {
|
|
||||||
guard_to.lock();
|
|
||||||
guard_from.lock();
|
|
||||||
} else {
|
|
||||||
// The vertices are the same vertex, only lock one.
|
|
||||||
guard_from.lock();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PrepareForWrite(&transaction_, from_vertex)) return Error::SERIALIZATION_ERROR;
|
|
||||||
MG_ASSERT(!from_vertex->deleted, "Invalid database state!");
|
|
||||||
|
|
||||||
if (to_vertex != from_vertex) {
|
|
||||||
if (!PrepareForWrite(&transaction_, to_vertex)) return Error::SERIALIZATION_ERROR;
|
|
||||||
MG_ASSERT(!to_vertex->deleted, "Invalid database state!");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto delete_edge_from_storage = [&edge_type, &edge_ref, this](auto *vertex, auto *edges) {
|
|
||||||
std::tuple<EdgeTypeId, Vertex *, EdgeRef> link(edge_type, vertex, edge_ref);
|
|
||||||
auto it = std::find(edges->begin(), edges->end(), link);
|
|
||||||
if (config_.properties_on_edges) {
|
|
||||||
MG_ASSERT(it != edges->end(), "Invalid database state!");
|
|
||||||
} else if (it == edges->end()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::swap(*it, *edges->rbegin());
|
|
||||||
edges->pop_back();
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto op1 = delete_edge_from_storage(to_vertex, &from_vertex->out_edges);
|
|
||||||
auto op2 = delete_edge_from_storage(from_vertex, &to_vertex->in_edges);
|
|
||||||
|
|
||||||
if (config_.properties_on_edges) {
|
|
||||||
MG_ASSERT((op1 && op2), "Invalid database state!");
|
|
||||||
} else {
|
|
||||||
MG_ASSERT((op1 && op2) || (!op1 && !op2), "Invalid database state!");
|
|
||||||
if (!op1 && !op2) {
|
|
||||||
// The edge is already deleted.
|
|
||||||
return std::optional<EdgeAccessor>{};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config_.properties_on_edges) {
|
|
||||||
auto *edge_ptr = edge_ref.ptr;
|
|
||||||
CreateAndLinkDelta(&transaction_, edge_ptr, Delta::RecreateObjectTag());
|
|
||||||
edge_ptr->deleted = true;
|
|
||||||
|
|
||||||
// Need to inform the next CollectGarbage call that there are some
|
|
||||||
// non-transactional deletions that need to be collected
|
|
||||||
if (transaction_.storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
|
|
||||||
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
|
|
||||||
mem_storage->gc_full_scan_edges_delete_ = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateAndLinkDelta(&transaction_, from_vertex, Delta::AddOutEdgeTag(), edge_type, to_vertex, edge_ref);
|
|
||||||
CreateAndLinkDelta(&transaction_, to_vertex, Delta::AddInEdgeTag(), edge_type, from_vertex, edge_ref);
|
|
||||||
|
|
||||||
transaction_.manyDeltasCache.Invalidate(from_vertex, edge_type, EdgeDirection::OUT);
|
|
||||||
transaction_.manyDeltasCache.Invalidate(to_vertex, edge_type, EdgeDirection::IN);
|
|
||||||
|
|
||||||
// Decrement edge count.
|
|
||||||
storage_->edge_count_.fetch_add(-1, std::memory_order_acq_rel);
|
|
||||||
|
|
||||||
return std::make_optional<EdgeAccessor>(edge_ref, edge_type, from_vertex, to_vertex, &transaction_,
|
|
||||||
&storage_->indices_, &storage_->constraints_, config_, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOLINTNEXTLINE(google-default-arguments)
|
// NOLINTNEXTLINE(google-default-arguments)
|
||||||
utils::BasicResult<StorageDataManipulationError, void> InMemoryStorage::InMemoryAccessor::Commit(
|
utils::BasicResult<StorageDataManipulationError, void> InMemoryStorage::InMemoryAccessor::Commit(
|
||||||
const std::optional<uint64_t> desired_commit_timestamp) {
|
const std::optional<uint64_t> desired_commit_timestamp) {
|
||||||
|
@ -205,14 +205,8 @@ class InMemoryStorage final : public Storage {
|
|||||||
labels);
|
labels);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @return Accessor to the deleted vertex if a deletion took place, std::nullopt otherwise
|
Result<std::optional<std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>>> DetachDelete(
|
||||||
/// @throw std::bad_alloc
|
std::vector<VertexAccessor *> nodes, std::vector<EdgeAccessor *> edges, bool detach) override;
|
||||||
Result<std::optional<VertexAccessor>> DeleteVertex(VertexAccessor *vertex) override;
|
|
||||||
|
|
||||||
/// @return Accessor to the deleted vertex and deleted edges if a deletion took place, std::nullopt otherwise
|
|
||||||
/// @throw std::bad_alloc
|
|
||||||
Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> DetachDeleteVertex(
|
|
||||||
VertexAccessor *vertex) override;
|
|
||||||
|
|
||||||
void PrefetchInEdges(const VertexAccessor &vertex_acc) override{};
|
void PrefetchInEdges(const VertexAccessor &vertex_acc) override{};
|
||||||
|
|
||||||
@ -225,10 +219,6 @@ class InMemoryStorage final : public Storage {
|
|||||||
|
|
||||||
Result<EdgeAccessor> EdgeSetTo(EdgeAccessor *edge, VertexAccessor *new_to) override;
|
Result<EdgeAccessor> EdgeSetTo(EdgeAccessor *edge, VertexAccessor *new_to) override;
|
||||||
|
|
||||||
/// Accessor to the deleted edge if a deletion took place, std::nullopt otherwise
|
|
||||||
/// @throw std::bad_alloc
|
|
||||||
Result<std::optional<EdgeAccessor>> DeleteEdge(EdgeAccessor *edge) override;
|
|
||||||
|
|
||||||
bool LabelIndexExists(LabelId label) const override {
|
bool LabelIndexExists(LabelId label) const override {
|
||||||
return static_cast<InMemoryStorage *>(storage_)->indices_.label_index_->IndexExists(label);
|
return static_cast<InMemoryStorage *>(storage_)->indices_.label_index_->IndexExists(label);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
// by the Apache License, Version 2.0, included in the file
|
// by the Apache License, Version 2.0, included in the file
|
||||||
// licenses/APL.txt.
|
// licenses/APL.txt.
|
||||||
|
|
||||||
|
#include "absl/container/flat_hash_set.h"
|
||||||
#include "spdlog/spdlog.h"
|
#include "spdlog/spdlog.h"
|
||||||
|
|
||||||
#include "storage/v2/disk/name_id_mapper.hpp"
|
#include "storage/v2/disk/name_id_mapper.hpp"
|
||||||
@ -127,4 +128,368 @@ void Storage::Accessor::AdvanceCommand() {
|
|||||||
++transaction_.command_id;
|
++transaction_.command_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result<std::optional<VertexAccessor>> Storage::Accessor::DeleteVertex(VertexAccessor *vertex) {
|
||||||
|
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>>;
|
||||||
|
|
||||||
|
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>>;
|
||||||
|
// 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_;
|
||||||
|
|
||||||
|
{
|
||||||
|
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_.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) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
std::unique_lock<utils::RWSpinLock> guard;
|
||||||
|
if (storage_->config_.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!");
|
||||||
|
|
||||||
|
attached_edges_to_vertex->pop_back();
|
||||||
|
if (storage_->config_.items.properties_on_edges) {
|
||||||
|
auto *edge_ptr = edge_ref.ptr;
|
||||||
|
MarkEdgeAsDeleted(edge_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const edge_gid = storage_->config_.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, &transaction_, &storage_->indices_,
|
||||||
|
&storage_->constraints_, storage_->config_.items, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateAndLinkDelta(&transaction_, vertex_ptr, deletion_delta, edge_type, opposing_vertex, edge_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_.items.properties_on_edges ? edge_ref.ptr->gid : edge_ref.gid;
|
||||||
|
return !set_for_erasure.contains(edge_gid);
|
||||||
|
});
|
||||||
|
|
||||||
|
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_.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_.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 ? 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, &transaction_, &storage_->indices_,
|
||||||
|
&storage_->constraints_, storage_->config_.items, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edges_attached_to_vertex->erase(mid, edges_attached_to_vertex->end());
|
||||||
|
|
||||||
|
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, &transaction_, &storage_->indices_, &storage_->constraints_,
|
||||||
|
storage_->config_.items, 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
|
} // namespace memgraph::storage
|
||||||
|
@ -68,6 +68,13 @@ struct StorageInfo {
|
|||||||
uint64_t disk_usage;
|
uint64_t disk_usage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct EdgeInfoForDeletion {
|
||||||
|
std::unordered_set<Gid> partial_src_edge_ids{};
|
||||||
|
std::unordered_set<Gid> partial_dest_edge_ids{};
|
||||||
|
std::unordered_set<Vertex *> partial_src_vertices{};
|
||||||
|
std::unordered_set<Vertex *> partial_dest_vertices{};
|
||||||
|
};
|
||||||
|
|
||||||
class Storage {
|
class Storage {
|
||||||
friend class ReplicationServer;
|
friend class ReplicationServer;
|
||||||
friend class ReplicationClient;
|
friend class ReplicationClient;
|
||||||
@ -111,6 +118,14 @@ class Storage {
|
|||||||
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
|
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
|
||||||
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) = 0;
|
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) = 0;
|
||||||
|
|
||||||
|
virtual Result<std::optional<VertexAccessor>> DeleteVertex(VertexAccessor *vertex);
|
||||||
|
|
||||||
|
virtual Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> DetachDeleteVertex(
|
||||||
|
VertexAccessor *vertex);
|
||||||
|
|
||||||
|
virtual Result<std::optional<std::pair<std::vector<VertexAccessor>, std::vector<EdgeAccessor>>>> DetachDelete(
|
||||||
|
std::vector<VertexAccessor *> nodes, std::vector<EdgeAccessor *> edges, bool detach);
|
||||||
|
|
||||||
virtual uint64_t ApproximateVertexCount() const = 0;
|
virtual uint64_t ApproximateVertexCount() const = 0;
|
||||||
|
|
||||||
virtual uint64_t ApproximateVertexCount(LabelId label) const = 0;
|
virtual uint64_t ApproximateVertexCount(LabelId label) const = 0;
|
||||||
@ -142,11 +157,6 @@ class Storage {
|
|||||||
|
|
||||||
virtual std::vector<LabelId> DeleteLabelIndexStats(std::span<std::string> labels) = 0;
|
virtual std::vector<LabelId> DeleteLabelIndexStats(std::span<std::string> labels) = 0;
|
||||||
|
|
||||||
virtual Result<std::optional<VertexAccessor>> DeleteVertex(VertexAccessor *vertex) = 0;
|
|
||||||
|
|
||||||
virtual Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> DetachDeleteVertex(
|
|
||||||
VertexAccessor *vertex) = 0;
|
|
||||||
|
|
||||||
virtual void PrefetchInEdges(const VertexAccessor &vertex_acc) = 0;
|
virtual void PrefetchInEdges(const VertexAccessor &vertex_acc) = 0;
|
||||||
|
|
||||||
virtual void PrefetchOutEdges(const VertexAccessor &vertex_acc) = 0;
|
virtual void PrefetchOutEdges(const VertexAccessor &vertex_acc) = 0;
|
||||||
@ -157,7 +167,7 @@ class Storage {
|
|||||||
|
|
||||||
virtual Result<EdgeAccessor> EdgeSetTo(EdgeAccessor *edge, VertexAccessor *new_to) = 0;
|
virtual Result<EdgeAccessor> EdgeSetTo(EdgeAccessor *edge, VertexAccessor *new_to) = 0;
|
||||||
|
|
||||||
virtual Result<std::optional<EdgeAccessor>> DeleteEdge(EdgeAccessor *edge) = 0;
|
virtual Result<std::optional<EdgeAccessor>> DeleteEdge(EdgeAccessor *edge);
|
||||||
|
|
||||||
virtual bool LabelIndexExists(LabelId label) const = 0;
|
virtual bool LabelIndexExists(LabelId label) const = 0;
|
||||||
|
|
||||||
@ -202,6 +212,18 @@ class Storage {
|
|||||||
std::optional<uint64_t> commit_timestamp_;
|
std::optional<uint64_t> commit_timestamp_;
|
||||||
bool is_transaction_active_;
|
bool is_transaction_active_;
|
||||||
|
|
||||||
|
// Detach delete private methods
|
||||||
|
Result<std::optional<std::unordered_set<Vertex *>>> PrepareDeletableNodes(
|
||||||
|
const std::vector<VertexAccessor *> &vertices);
|
||||||
|
EdgeInfoForDeletion PrepareDeletableEdges(const std::unordered_set<Vertex *> &vertices,
|
||||||
|
const std::vector<EdgeAccessor *> &edges, bool detach) noexcept;
|
||||||
|
Result<std::optional<std::vector<EdgeAccessor>>> ClearEdgesOnVertices(const std::unordered_set<Vertex *> &vertices,
|
||||||
|
std::unordered_set<Gid> &deleted_edge_ids);
|
||||||
|
Result<std::optional<std::vector<EdgeAccessor>>> DetachRemainingEdges(
|
||||||
|
EdgeInfoForDeletion info, std::unordered_set<Gid> &partially_detached_edge_ids);
|
||||||
|
Result<std::vector<VertexAccessor>> TryDeleteVertices(const std::unordered_set<Vertex *> &vertices);
|
||||||
|
void MarkEdgeAsDeleted(Edge *edge);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
StorageMode creation_storage_mode_;
|
StorageMode creation_storage_mode_;
|
||||||
};
|
};
|
||||||
|
211
tests/gql_behave/tests/memgraph_V1/features/delete.feature
Normal file
211
tests/gql_behave/tests/memgraph_V1/features/delete.feature
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
Feature: Delete
|
||||||
|
|
||||||
|
Scenario: Delete all from database and yield number of deleted items
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n), (m), (o)
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n RETURN COUNT(*) AS cnt
|
||||||
|
"""
|
||||||
|
Then the result should be:
|
||||||
|
| cnt |
|
||||||
|
| 3 |
|
||||||
|
|
||||||
|
Scenario: Delete all from database and match nothing after
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n), (m)
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n WITH n MATCH (m) RETURN COUNT(*) AS cnt
|
||||||
|
"""
|
||||||
|
Then the result should be:
|
||||||
|
| cnt |
|
||||||
|
| 0 |
|
||||||
|
|
||||||
|
Scenario: Delete relationship in the pattern
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n)-[:REL]->(m), (k)-[:REL]->(z)
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n)-[r:REL]->(m) DELETE r RETURN COUNT(*) AS cnt
|
||||||
|
"""
|
||||||
|
Then the result should be:
|
||||||
|
| cnt |
|
||||||
|
| 2 |
|
||||||
|
|
||||||
|
Scenario: Delete node and return property throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n RETURN n.prop AS prop
|
||||||
|
"""
|
||||||
|
Then an error should be raised
|
||||||
|
|
||||||
|
Scenario: Delete node, set property throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n SET n.prop = 2
|
||||||
|
"""
|
||||||
|
Then the result should be empty
|
||||||
|
|
||||||
|
Scenario: Delete node, set property and return throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n SET n.prop = 2 RETURN n
|
||||||
|
"""
|
||||||
|
Then an error should be raised
|
||||||
|
|
||||||
|
Scenario: Delete node, remove property throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n REMOVE n.prop
|
||||||
|
"""
|
||||||
|
Then the result should be empty
|
||||||
|
|
||||||
|
Scenario: Delete node, remove property and return throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n REMOVE n.prop RETURN n
|
||||||
|
"""
|
||||||
|
Then an error should be raised
|
||||||
|
|
||||||
|
Scenario: Delete node, set label throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n SET n:Label
|
||||||
|
"""
|
||||||
|
Then the result should be empty
|
||||||
|
|
||||||
|
Scenario: Delete node, set label and return throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n SET n:Label RETURN n
|
||||||
|
"""
|
||||||
|
Then an error should be raised
|
||||||
|
|
||||||
|
Scenario: Delete node, remove label throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n:Label {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n REMOVE n:Label
|
||||||
|
"""
|
||||||
|
Then the result should be empty
|
||||||
|
|
||||||
|
Scenario: Delete node, remove label and return throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n:Label {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n REMOVE n:Label RETURN n
|
||||||
|
"""
|
||||||
|
Then an error should be raised
|
||||||
|
|
||||||
|
Scenario: Delete node, set update properties and return throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n:Label {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n SET n += {prop: 2} RETURN n
|
||||||
|
"""
|
||||||
|
Then an error should be raised
|
||||||
|
|
||||||
|
Scenario: Delete node, set properties throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n:Label {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n SET n += {prop: 2}
|
||||||
|
"""
|
||||||
|
Then the result should be empty
|
||||||
|
|
||||||
|
Scenario: Delete node, set replace properties and return throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n:Label {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n SET n = {prop: 2} RETURN n
|
||||||
|
"""
|
||||||
|
Then an error should be raised
|
||||||
|
|
||||||
|
Scenario: Delete node, set replace properties throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n:Label {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n SET n = {prop: 2}
|
||||||
|
"""
|
||||||
|
Then the result should be empty
|
||||||
|
|
||||||
|
Scenario: Delete node, set property and return it with aggregation throws an error
|
||||||
|
Given an empty graph
|
||||||
|
And having executed
|
||||||
|
"""
|
||||||
|
CREATE (n:Label {prop: 1});
|
||||||
|
"""
|
||||||
|
When executing query:
|
||||||
|
"""
|
||||||
|
MATCH (n) DETACH DELETE n SET n.prop = 1 WITH n RETURN n
|
||||||
|
"""
|
||||||
|
Then an error should be raised
|
@ -12,44 +12,51 @@
|
|||||||
# by the Apache License, Version 2.0, included in the file
|
# by the Apache License, Version 2.0, included in the file
|
||||||
# licenses/APL.txt.
|
# licenses/APL.txt.
|
||||||
|
|
||||||
'''
|
"""
|
||||||
Large bipartite graph stress test.
|
Large bipartite graph stress test.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import time
|
import time
|
||||||
import atexit
|
|
||||||
|
|
||||||
from common import connection_argument_parser, assert_equal, \
|
from common import (
|
||||||
OutputData, execute_till_success, \
|
OutputData,
|
||||||
batch, render, SessionCache
|
SessionCache,
|
||||||
|
assert_equal,
|
||||||
|
batch,
|
||||||
|
connection_argument_parser,
|
||||||
|
execute_till_success,
|
||||||
|
render,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
'''
|
"""
|
||||||
Parses user arguments
|
Parses user arguments
|
||||||
|
|
||||||
:return: parsed arguments
|
:return: parsed arguments
|
||||||
'''
|
"""
|
||||||
parser = connection_argument_parser()
|
parser = connection_argument_parser()
|
||||||
parser.add_argument('--worker-count', type=int,
|
parser.add_argument(
|
||||||
default=multiprocessing.cpu_count(),
|
"--worker-count", type=int, default=multiprocessing.cpu_count(), help="Number of concurrent workers."
|
||||||
help='Number of concurrent workers.')
|
)
|
||||||
parser.add_argument("--logging", default="INFO",
|
parser.add_argument(
|
||||||
choices=["INFO", "DEBUG", "WARNING", "ERROR"],
|
"--logging", default="INFO", choices=["INFO", "DEBUG", "WARNING", "ERROR"], help="Logging level"
|
||||||
help="Logging level")
|
)
|
||||||
parser.add_argument('--u-count', type=int, default=100,
|
parser.add_argument("--u-count", type=int, default=100, help="Size of U set in the bipartite graph.")
|
||||||
help='Size of U set in the bipartite graph.')
|
parser.add_argument("--v-count", type=int, default=100, help="Size of V set in the bipartite graph.")
|
||||||
parser.add_argument('--v-count', type=int, default=100,
|
parser.add_argument("--vertex-batch-size", type=int, default=100, help="Create vertices in batches of this size.")
|
||||||
help='Size of V set in the bipartite graph.')
|
parser.add_argument("--edge-batching", action="store_true", help="Create edges in batches.")
|
||||||
parser.add_argument('--vertex-batch-size', type=int, default=100,
|
parser.add_argument(
|
||||||
help="Create vertices in batches of this size.")
|
"--edge-batch-size",
|
||||||
parser.add_argument('--edge-batching', action='store_true',
|
type=int,
|
||||||
help='Create edges in batches.')
|
default=100,
|
||||||
parser.add_argument('--edge-batch-size', type=int, default=100,
|
help="Number of edges in a batch when edges " "are created in batches.",
|
||||||
help='Number of edges in a batch when edges '
|
)
|
||||||
'are created in batches.')
|
parser.add_argument("--isolation-level", type=str, required=True, help="Database isolation level.")
|
||||||
|
parser.add_argument("--storage-mode", type=str, required=True, help="Database storage mode.")
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@ -62,18 +69,18 @@ atexit.register(SessionCache.cleanup)
|
|||||||
|
|
||||||
|
|
||||||
def create_u_v_edges(u):
|
def create_u_v_edges(u):
|
||||||
'''
|
"""
|
||||||
Creates nodes and checks that all nodes were created.
|
Creates nodes and checks that all nodes were created.
|
||||||
create edges from one vertex in U set to all vertex of V set
|
create edges from one vertex in U set to all vertex of V set
|
||||||
|
|
||||||
:param worker_id: worker id
|
:param worker_id: worker id
|
||||||
|
|
||||||
:return: tuple (worker_id, create execution time, time unit)
|
:return: tuple (worker_id, create execution time, time unit)
|
||||||
'''
|
"""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
session = SessionCache.argument_session(args)
|
session = SessionCache.argument_session(args)
|
||||||
no_failures = 0
|
no_failures = 0
|
||||||
match_u = 'MATCH (u:U {id: %d})' % u
|
match_u = "MATCH (u:U {id: %d})" % u
|
||||||
if args.edge_batching:
|
if args.edge_batching:
|
||||||
# TODO: try to randomize execution, the execution time should
|
# TODO: try to randomize execution, the execution time should
|
||||||
# be smaller, add randomize flag
|
# be smaller, add randomize flag
|
||||||
@ -83,143 +90,120 @@ def create_u_v_edges(u):
|
|||||||
query = match_u + "".join(match_v) + "".join(create_u)
|
query = match_u + "".join(match_v) + "".join(create_u)
|
||||||
no_failures += execute_till_success(session, query)[1]
|
no_failures += execute_till_success(session, query)[1]
|
||||||
else:
|
else:
|
||||||
no_failures += execute_till_success(
|
no_failures += execute_till_success(session, match_u + " MATCH (v:V) CREATE (u)-[:R]->(v)")[1]
|
||||||
session, match_u + ' MATCH (v:V) CREATE (u)-[:R]->(v)')[1]
|
|
||||||
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
return u, end_time - start_time, "s", no_failures
|
return u, end_time - start_time, "s", no_failures
|
||||||
|
|
||||||
|
|
||||||
def traverse_from_u_worker(u):
|
def traverse_from_u_worker(u):
|
||||||
'''
|
"""
|
||||||
Traverses edges starting from an element of U set.
|
Traverses edges starting from an element of U set.
|
||||||
Traversed labels are: :U -> :V -> :U.
|
Traversed labels are: :U -> :V -> :U.
|
||||||
'''
|
"""
|
||||||
session = SessionCache.argument_session(args)
|
session = SessionCache.argument_session(args)
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
assert_equal(
|
assert_equal(
|
||||||
args.u_count * args.v_count - args.v_count, # cypher morphism
|
args.u_count * args.v_count - args.v_count, # cypher morphism
|
||||||
session.run("MATCH (u1:U {id: %s})-[e1]->(v:V)<-[e2]-(u2:U) "
|
session.run("MATCH (u1:U {id: %s})-[e1]->(v:V)<-[e2]-(u2:U) " "RETURN count(v) AS cnt" % u).data()[0]["cnt"],
|
||||||
"RETURN count(v) AS cnt" % u).data()[0]['cnt'],
|
"Number of traversed edges started " "from U(id:%s) is wrong!. " % u + "Expected: %s Actual: %s",
|
||||||
"Number of traversed edges started "
|
)
|
||||||
"from U(id:%s) is wrong!. " % u +
|
|
||||||
"Expected: %s Actual: %s")
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
return u, end_time - start_time, 's'
|
return u, end_time - start_time, "s"
|
||||||
|
|
||||||
|
|
||||||
def traverse_from_v_worker(v):
|
def traverse_from_v_worker(v):
|
||||||
'''
|
"""
|
||||||
Traverses edges starting from an element of V set.
|
Traverses edges starting from an element of V set.
|
||||||
Traversed labels are: :V -> :U -> :V.
|
Traversed labels are: :V -> :U -> :V.
|
||||||
'''
|
"""
|
||||||
session = SessionCache.argument_session(args)
|
session = SessionCache.argument_session(args)
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
assert_equal(
|
assert_equal(
|
||||||
args.u_count * args.v_count - args.u_count, # cypher morphism
|
args.u_count * args.v_count - args.u_count, # cypher morphism
|
||||||
session.run("MATCH (v1:V {id: %s})<-[e1]-(u:U)-[e2]->(v2:V) "
|
session.run("MATCH (v1:V {id: %s})<-[e1]-(u:U)-[e2]->(v2:V) " "RETURN count(u) AS cnt" % v).data()[0]["cnt"],
|
||||||
"RETURN count(u) AS cnt" % v).data()[0]['cnt'],
|
"Number of traversed edges started " "from V(id:%s) is wrong!. " % v + "Expected: %s Actual: %s",
|
||||||
"Number of traversed edges started "
|
)
|
||||||
"from V(id:%s) is wrong!. " % v +
|
|
||||||
"Expected: %s Actual: %s")
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
return v, end_time - start_time, 's'
|
return v, end_time - start_time, "s"
|
||||||
|
|
||||||
|
|
||||||
def execution_handler():
|
def execution_handler():
|
||||||
'''
|
"""
|
||||||
Initializes client processes, database and starts the execution.
|
Initializes client processes, database and starts the execution.
|
||||||
'''
|
"""
|
||||||
# instance cleanup
|
# instance cleanup
|
||||||
session = SessionCache.argument_session(args)
|
session = SessionCache.argument_session(args)
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# clean existing database
|
# clean existing database
|
||||||
session.run('MATCH (n) DETACH DELETE n').consume()
|
session.run("MATCH (n) DETACH DELETE n").consume()
|
||||||
cleanup_end_time = time.time()
|
cleanup_end_time = time.time()
|
||||||
output_data.add_measurement("cleanup_time",
|
output_data.add_measurement("cleanup_time", cleanup_end_time - start_time)
|
||||||
cleanup_end_time - start_time)
|
|
||||||
log.info("Database is clean.")
|
log.info("Database is clean.")
|
||||||
|
|
||||||
# create indices
|
# create indices
|
||||||
session.run('CREATE INDEX ON :U').consume()
|
session.run("CREATE INDEX ON :U").consume()
|
||||||
session.run('CREATE INDEX ON :V').consume()
|
session.run("CREATE INDEX ON :V").consume()
|
||||||
|
|
||||||
# create U vertices
|
# create U vertices
|
||||||
for b in batch(render('CREATE (:U {{id: {}}})', range(args.u_count)),
|
for b in batch(render("CREATE (:U {{id: {}}})", range(args.u_count)), args.vertex_batch_size):
|
||||||
args.vertex_batch_size):
|
|
||||||
session.run(" ".join(b)).consume()
|
session.run(" ".join(b)).consume()
|
||||||
# create V vertices
|
# create V vertices
|
||||||
for b in batch(render('CREATE (:V {{id: {}}})', range(args.v_count)),
|
for b in batch(render("CREATE (:V {{id: {}}})", range(args.v_count)), args.vertex_batch_size):
|
||||||
args.vertex_batch_size):
|
|
||||||
session.run(" ".join(b)).consume()
|
session.run(" ".join(b)).consume()
|
||||||
vertices_create_end_time = time.time()
|
vertices_create_end_time = time.time()
|
||||||
output_data.add_measurement(
|
output_data.add_measurement("vertices_create_time", vertices_create_end_time - cleanup_end_time)
|
||||||
'vertices_create_time',
|
|
||||||
vertices_create_end_time - cleanup_end_time)
|
|
||||||
log.info("All nodes created.")
|
log.info("All nodes created.")
|
||||||
|
|
||||||
# concurrent create execution & tests
|
# concurrent create execution & tests
|
||||||
with multiprocessing.Pool(args.worker_count) as p:
|
with multiprocessing.Pool(args.worker_count) as p:
|
||||||
create_edges_start_time = time.time()
|
create_edges_start_time = time.time()
|
||||||
for worker_id, create_time, time_unit, no_failures in \
|
for worker_id, create_time, time_unit, no_failures in p.map(create_u_v_edges, [i for i in range(args.u_count)]):
|
||||||
p.map(create_u_v_edges, [i for i in range(args.u_count)]):
|
log.info("Worker ID: %s; Create time: %s%s Failures: %s" % (worker_id, create_time, time_unit, no_failures))
|
||||||
log.info('Worker ID: %s; Create time: %s%s Failures: %s' %
|
|
||||||
(worker_id, create_time, time_unit, no_failures))
|
|
||||||
create_edges_end_time = time.time()
|
create_edges_end_time = time.time()
|
||||||
output_data.add_measurement(
|
output_data.add_measurement("edges_create_time", create_edges_end_time - create_edges_start_time)
|
||||||
'edges_create_time',
|
|
||||||
create_edges_end_time - create_edges_start_time)
|
|
||||||
|
|
||||||
# check total number of edges
|
# check total number of edges
|
||||||
assert_equal(
|
assert_equal(
|
||||||
args.v_count * args.u_count,
|
args.v_count * args.u_count,
|
||||||
session.run(
|
session.run("MATCH ()-[r]->() " "RETURN count(r) AS cnt").data()[0]["cnt"],
|
||||||
'MATCH ()-[r]->() '
|
"Total number of edges isn't correct! Expected: %s Actual: %s",
|
||||||
'RETURN count(r) AS cnt').data()[0]['cnt'],
|
)
|
||||||
"Total number of edges isn't correct! Expected: %s Actual: %s")
|
|
||||||
|
|
||||||
# check traversals starting from all elements of U
|
# check traversals starting from all elements of U
|
||||||
traverse_from_u_start_time = time.time()
|
traverse_from_u_start_time = time.time()
|
||||||
for u, traverse_u_time, time_unit in \
|
for u, traverse_u_time, time_unit in p.map(traverse_from_u_worker, [i for i in range(args.u_count)]):
|
||||||
p.map(traverse_from_u_worker,
|
|
||||||
[i for i in range(args.u_count)]):
|
|
||||||
log.info("U {id: %s} %s%s" % (u, traverse_u_time, time_unit))
|
log.info("U {id: %s} %s%s" % (u, traverse_u_time, time_unit))
|
||||||
traverse_from_u_end_time = time.time()
|
traverse_from_u_end_time = time.time()
|
||||||
output_data.add_measurement(
|
output_data.add_measurement("traverse_from_u_time", traverse_from_u_end_time - traverse_from_u_start_time)
|
||||||
'traverse_from_u_time',
|
|
||||||
traverse_from_u_end_time - traverse_from_u_start_time)
|
|
||||||
|
|
||||||
# check traversals starting from all elements of V
|
# check traversals starting from all elements of V
|
||||||
traverse_from_v_start_time = time.time()
|
traverse_from_v_start_time = time.time()
|
||||||
for v, traverse_v_time, time_unit in \
|
for v, traverse_v_time, time_unit in p.map(traverse_from_v_worker, [i for i in range(args.v_count)]):
|
||||||
p.map(traverse_from_v_worker,
|
|
||||||
[i for i in range(args.v_count)]):
|
|
||||||
log.info("V {id: %s} %s%s" % (v, traverse_v_time, time_unit))
|
log.info("V {id: %s} %s%s" % (v, traverse_v_time, time_unit))
|
||||||
traverse_from_v_end_time = time.time()
|
traverse_from_v_end_time = time.time()
|
||||||
output_data.add_measurement(
|
output_data.add_measurement("traverse_from_v_time", traverse_from_v_end_time - traverse_from_v_start_time)
|
||||||
'traverse_from_v_time',
|
|
||||||
traverse_from_v_end_time - traverse_from_v_start_time)
|
|
||||||
|
|
||||||
# check total number of vertices
|
# check total number of vertices
|
||||||
assert_equal(
|
assert_equal(
|
||||||
args.v_count + args.u_count,
|
args.v_count + args.u_count,
|
||||||
session.run('MATCH (n) RETURN count(n) AS cnt').data()[0]['cnt'],
|
session.run("MATCH (n) RETURN count(n) AS cnt").data()[0]["cnt"],
|
||||||
"Total number of vertices isn't correct! Expected: %s Actual: %s")
|
"Total number of vertices isn't correct! Expected: %s Actual: %s",
|
||||||
|
)
|
||||||
|
|
||||||
# check total number of edges
|
# check total number of edges
|
||||||
assert_equal(
|
assert_equal(
|
||||||
args.v_count * args.u_count,
|
args.v_count * args.u_count,
|
||||||
session.run(
|
session.run("MATCH ()-[r]->() RETURN count(r) AS cnt").data()[0]["cnt"],
|
||||||
'MATCH ()-[r]->() RETURN count(r) AS cnt').data()[0]['cnt'],
|
"Total number of edges isn't correct! Expected: %s Actual: %s",
|
||||||
"Total number of edges isn't correct! Expected: %s Actual: %s")
|
)
|
||||||
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
output_data.add_measurement("total_execution_time",
|
output_data.add_measurement("total_execution_time", end_time - start_time)
|
||||||
end_time - start_time)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=args.logging)
|
logging.basicConfig(level=args.logging)
|
||||||
if args.logging != "DEBUG":
|
if args.logging != "DEBUG":
|
||||||
logging.getLogger("neo4j").setLevel(logging.WARNING)
|
logging.getLogger("neo4j").setLevel(logging.WARNING)
|
||||||
|
@ -11,26 +11,27 @@
|
|||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
'''
|
"""
|
||||||
Common methods for writing graph database
|
Common methods for writing graph database
|
||||||
integration tests in python.
|
integration tests in python.
|
||||||
|
|
||||||
Only Bolt communication protocol is supported.
|
Only Bolt communication protocol is supported.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import os
|
import os
|
||||||
from threading import Thread
|
import time
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from neo4j import GraphDatabase, TRUST_ALL_CERTIFICATES
|
from threading import Thread
|
||||||
|
|
||||||
|
from gqlalchemy import Memgraph
|
||||||
|
from neo4j import TRUST_ALL_CERTIFICATES, GraphDatabase
|
||||||
|
|
||||||
|
|
||||||
class OutputData:
|
class OutputData:
|
||||||
'''
|
"""
|
||||||
Encapsulates results and info about the tests.
|
Encapsulates results and info about the tests.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# data in time format (name, time, unit)
|
# data in time format (name, time, unit)
|
||||||
@ -39,32 +40,32 @@ class OutputData:
|
|||||||
self._statuses = []
|
self._statuses = []
|
||||||
|
|
||||||
def add_measurement(self, name, time, unit="s"):
|
def add_measurement(self, name, time, unit="s"):
|
||||||
'''
|
"""
|
||||||
Stores measurement.
|
Stores measurement.
|
||||||
|
|
||||||
:param name: str, name of measurement
|
:param name: str, name of measurement
|
||||||
:param time: float, time value
|
:param time: float, time value
|
||||||
:param unit: str, time unit
|
:param unit: str, time unit
|
||||||
'''
|
"""
|
||||||
self._measurements.append((name, time, unit))
|
self._measurements.append((name, time, unit))
|
||||||
|
|
||||||
def add_status(self, name, status):
|
def add_status(self, name, status):
|
||||||
'''
|
"""
|
||||||
Stores status data point.
|
Stores status data point.
|
||||||
|
|
||||||
:param name: str, name of data point
|
:param name: str, name of data point
|
||||||
:param status: printable value
|
:param status: printable value
|
||||||
'''
|
"""
|
||||||
self._statuses.append((name, status))
|
self._statuses.append((name, status))
|
||||||
|
|
||||||
def dump(self, print_f=print):
|
def dump(self, print_f=print):
|
||||||
'''
|
"""
|
||||||
Dumps output using the given ouput function.
|
Dumps output using the given ouput function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
print_f - the function that consumes ouptput. Defaults to
|
print_f - the function that consumes ouptput. Defaults to
|
||||||
the 'print' function.
|
the 'print' function.
|
||||||
'''
|
"""
|
||||||
print_f("Output data:")
|
print_f("Output data:")
|
||||||
for name, status in self._statuses:
|
for name, status in self._statuses:
|
||||||
print_f(" %s: %s" % (name, status))
|
print_f(" %s: %s" % (name, status))
|
||||||
@ -73,7 +74,7 @@ class OutputData:
|
|||||||
|
|
||||||
|
|
||||||
def execute_till_success(session, query, max_retries=1000):
|
def execute_till_success(session, query, max_retries=1000):
|
||||||
'''
|
"""
|
||||||
Executes a query within Bolt session until the query is
|
Executes a query within Bolt session until the query is
|
||||||
successfully executed against the database.
|
successfully executed against the database.
|
||||||
|
|
||||||
@ -86,7 +87,7 @@ def execute_till_success(session, query, max_retries=1000):
|
|||||||
:param query: query to execute
|
:param query: query to execute
|
||||||
|
|
||||||
:return: tuple (results_data_list, number_of_failures, result_summary)
|
:return: tuple (results_data_list, number_of_failures, result_summary)
|
||||||
'''
|
"""
|
||||||
no_failures = 0
|
no_failures = 0
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@ -97,12 +98,24 @@ def execute_till_success(session, query, max_retries=1000):
|
|||||||
except Exception:
|
except Exception:
|
||||||
no_failures += 1
|
no_failures += 1
|
||||||
if no_failures >= max_retries:
|
if no_failures >= max_retries:
|
||||||
raise Exception("Query '%s' failed %d times, aborting" %
|
raise Exception("Query '%s' failed %d times, aborting" % (query, max_retries))
|
||||||
(query, max_retries))
|
|
||||||
|
|
||||||
|
def execute_till_success_gqlalchemy(memgraph: Memgraph, query: str, max_retries=1000):
|
||||||
|
"""Same method as execute_till_success, but for gqlalchemy."""
|
||||||
|
no_failures = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
result = memgraph.execute(query)
|
||||||
|
return result, no_failures
|
||||||
|
except Exception:
|
||||||
|
no_failures += 1
|
||||||
|
if no_failures >= max_retries:
|
||||||
|
raise Exception("Query '%s' failed %d times, aborting" % (query, max_retries))
|
||||||
|
|
||||||
|
|
||||||
def batch(input, batch_size):
|
def batch(input, batch_size):
|
||||||
""" Batches the given input (must be iterable).
|
"""Batches the given input (must be iterable).
|
||||||
Supports input generators. Returns a generator.
|
Supports input generators. Returns a generator.
|
||||||
All is lazy. The last batch can contain less elements
|
All is lazy. The last batch can contain less elements
|
||||||
then `batch_size`, but is for sure more then zero.
|
then `batch_size`, but is for sure more then zero.
|
||||||
@ -134,7 +147,7 @@ def render(template, iterable_arguments):
|
|||||||
|
|
||||||
|
|
||||||
def assert_equal(expected, actual, message):
|
def assert_equal(expected, actual, message):
|
||||||
'''
|
"""
|
||||||
Compares expected and actual values. If values are not the same terminate
|
Compares expected and actual values. If values are not the same terminate
|
||||||
the execution.
|
the execution.
|
||||||
|
|
||||||
@ -142,45 +155,41 @@ def assert_equal(expected, actual, message):
|
|||||||
:param actual: actual value
|
:param actual: actual value
|
||||||
:param message: str, message in case that the values are not equal, must
|
:param message: str, message in case that the values are not equal, must
|
||||||
contain two placeholders (%s) to print the values.
|
contain two placeholders (%s) to print the values.
|
||||||
'''
|
"""
|
||||||
assert expected == actual, message % (expected, actual)
|
assert expected == actual, message % (expected, actual)
|
||||||
|
|
||||||
|
|
||||||
def connection_argument_parser():
|
def connection_argument_parser():
|
||||||
'''
|
"""
|
||||||
Parses arguments related to establishing database connection like
|
Parses arguments related to establishing database connection like
|
||||||
host, port, username, etc.
|
host, port, username, etc.
|
||||||
|
|
||||||
:return: An instance of ArgumentParser
|
:return: An instance of ArgumentParser
|
||||||
'''
|
"""
|
||||||
parser = ArgumentParser(description=__doc__)
|
parser = ArgumentParser(description=__doc__)
|
||||||
|
|
||||||
parser.add_argument('--endpoint', type=str, default='127.0.0.1:7687',
|
parser.add_argument(
|
||||||
help='DBMS instance endpoint. '
|
"--endpoint",
|
||||||
'Bolt protocol is the only option.')
|
type=str,
|
||||||
parser.add_argument('--username', type=str, default='neo4j',
|
default="127.0.0.1:7687",
|
||||||
help='DBMS instance username.')
|
help="DBMS instance endpoint. " "Bolt protocol is the only option.",
|
||||||
parser.add_argument('--password', type=int, default='1234',
|
)
|
||||||
help='DBMS instance password.')
|
parser.add_argument("--username", type=str, default="neo4j", help="DBMS instance username.")
|
||||||
parser.add_argument('--use-ssl', action='store_true',
|
parser.add_argument("--password", type=str, default="1234", help="DBMS instance password.")
|
||||||
help="Is SSL enabled?")
|
parser.add_argument("--use-ssl", action="store_true", help="Is SSL enabled?")
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def bolt_session(url, auth, ssl=False):
|
def bolt_session(url, auth, ssl=False):
|
||||||
'''
|
"""
|
||||||
with wrapper around Bolt session.
|
with wrapper around Bolt session.
|
||||||
|
|
||||||
:param url: str, e.g. "bolt://127.0.0.1:7687"
|
:param url: str, e.g. "bolt://127.0.0.1:7687"
|
||||||
:param auth: auth method, goes directly to the Bolt driver constructor
|
:param auth: auth method, goes directly to the Bolt driver constructor
|
||||||
:param ssl: bool, is ssl enabled
|
:param ssl: bool, is ssl enabled
|
||||||
'''
|
"""
|
||||||
driver = GraphDatabase.driver(
|
driver = GraphDatabase.driver(url, auth=auth, encrypted=ssl, trust=TRUST_ALL_CERTIFICATES)
|
||||||
url,
|
|
||||||
auth=auth,
|
|
||||||
encrypted=ssl,
|
|
||||||
trust=TRUST_ALL_CERTIFICATES)
|
|
||||||
session = driver.session()
|
session = driver.session()
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
@ -192,19 +201,37 @@ def bolt_session(url, auth, ssl=False):
|
|||||||
# If you are using session with multiprocessing take a look at SesssionCache
|
# If you are using session with multiprocessing take a look at SesssionCache
|
||||||
# in bipartite for an idea how to reuse sessions.
|
# in bipartite for an idea how to reuse sessions.
|
||||||
def argument_session(args):
|
def argument_session(args):
|
||||||
'''
|
"""
|
||||||
:return: Bolt session context manager based on program arguments
|
:return: Bolt session context manager based on program arguments
|
||||||
'''
|
"""
|
||||||
return bolt_session('bolt://' + args.endpoint,
|
return bolt_session("bolt://" + args.endpoint, (args.username, args.password), args.use_ssl)
|
||||||
(args.username, str(args.password)),
|
|
||||||
args.use_ssl)
|
|
||||||
|
|
||||||
|
|
||||||
def argument_driver(args):
|
def argument_driver(args):
|
||||||
return GraphDatabase.driver(
|
return GraphDatabase.driver(
|
||||||
'bolt://' + args.endpoint,
|
"bolt://" + args.endpoint,
|
||||||
auth=(args.username, str(args.password)),
|
auth=(args.username, args.password),
|
||||||
encrypted=args.use_ssl, trust=TRUST_ALL_CERTIFICATES)
|
encrypted=args.use_ssl,
|
||||||
|
trust=TRUST_ALL_CERTIFICATES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_memgraph(args) -> Memgraph:
|
||||||
|
host_port = args.endpoint.split(":")
|
||||||
|
|
||||||
|
connection_params = {
|
||||||
|
"host": host_port[0],
|
||||||
|
"port": int(host_port[1]),
|
||||||
|
"username": args.username,
|
||||||
|
"password": args.password,
|
||||||
|
"encrypted": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.use_ssl:
|
||||||
|
connection_params["encrypted"] = True
|
||||||
|
|
||||||
|
return Memgraph(**connection_params)
|
||||||
|
|
||||||
|
|
||||||
# This class is used to create and cache sessions. Session is cached by args
|
# This class is used to create and cache sessions. Session is cached by args
|
||||||
# used to create it and process' pid in which it was created. This makes it
|
# used to create it and process' pid in which it was created. This makes it
|
||||||
@ -219,8 +246,8 @@ class SessionCache:
|
|||||||
key = tuple(vars(args).items()) + (os.getpid(),)
|
key = tuple(vars(args).items()) + (os.getpid(),)
|
||||||
if key in SessionCache.cache:
|
if key in SessionCache.cache:
|
||||||
return SessionCache.cache[key][1]
|
return SessionCache.cache[key][1]
|
||||||
driver = argument_driver(args) # |
|
driver = argument_driver(args) # |
|
||||||
session = driver.session() # V
|
session = driver.session() # V
|
||||||
SessionCache.cache[key] = (driver, session)
|
SessionCache.cache[key] = (driver, session)
|
||||||
return session
|
return session
|
||||||
|
|
||||||
@ -241,9 +268,10 @@ def periodically_execute(callable, args, interval, daemon=True):
|
|||||||
interval - time (in seconds) between two calls
|
interval - time (in seconds) between two calls
|
||||||
deamon - if the execution thread should be a daemon
|
deamon - if the execution thread should be a daemon
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def periodic_call():
|
def periodic_call():
|
||||||
while True:
|
while True:
|
||||||
sleep(interval)
|
time.sleep(interval)
|
||||||
callable()
|
callable()
|
||||||
|
|
||||||
Thread(target=periodic_call, args=args, daemon=daemon).start()
|
Thread(target=periodic_call, args=args, daemon=daemon).start()
|
||||||
|
@ -1,81 +1,16 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
import copy
|
||||||
import json
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
|
from argparse import Namespace as Args
|
||||||
|
from subprocess import Popen
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
# dataset calibrated for running on Apollo (total 4min)
|
from test_config import LARGE_DATASET, SMALL_DATASET, DatabaseMode, DatasetConstants
|
||||||
# bipartite.py runs for approx. 30s
|
|
||||||
# create_match.py runs for approx. 30s
|
|
||||||
# long_running runs for 1min
|
|
||||||
# long_running runs for 2min
|
|
||||||
SMALL_DATASET = [
|
|
||||||
{
|
|
||||||
"test": "bipartite.py",
|
|
||||||
"options": ["--u-count", "100", "--v-count", "100"],
|
|
||||||
"timeout": 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"test": "create_match.py",
|
|
||||||
"options": ["--vertex-count", "40000", "--create-pack-size", "100"],
|
|
||||||
"timeout": 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"test": "parser.cpp",
|
|
||||||
"options": ["--per-worker-query-count", "1000"],
|
|
||||||
"timeout": 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"test": "long_running.cpp",
|
|
||||||
"options": ["--vertex-count", "1000", "--edge-count", "5000", "--max-time", "1", "--verify", "20"],
|
|
||||||
"timeout": 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"test": "long_running.cpp",
|
|
||||||
"options": ["--vertex-count", "10000", "--edge-count", "50000", "--max-time", "2", "--verify", "30"],
|
|
||||||
"timeout": 5,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# dataset calibrated for running on daily stress instance (total 9h)
|
|
||||||
# bipartite.py and create_match.py run for approx. 15min
|
|
||||||
# long_running runs for 5min x 6 times = 30min
|
|
||||||
# long_running runs for 8h
|
|
||||||
LARGE_DATASET = (
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"test": "bipartite.py",
|
|
||||||
"options": ["--u-count", "300", "--v-count", "300"],
|
|
||||||
"timeout": 30,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"test": "create_match.py",
|
|
||||||
"options": ["--vertex-count", "500000", "--create-pack-size", "500"],
|
|
||||||
"timeout": 30,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
+ [
|
|
||||||
{
|
|
||||||
"test": "long_running.cpp",
|
|
||||||
"options": ["--vertex-count", "10000", "--edge-count", "40000", "--max-time", "5", "--verify", "60"],
|
|
||||||
"timeout": 16,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
* 6
|
|
||||||
+ [
|
|
||||||
{
|
|
||||||
"test": "long_running.cpp",
|
|
||||||
"options": ["--vertex-count", "200000", "--edge-count", "1000000", "--max-time", "480", "--verify", "300"],
|
|
||||||
"timeout": 500,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# paths
|
# paths
|
||||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
@ -87,64 +22,12 @@ CERT_FILE = os.path.join(SCRIPT_DIR, ".cert.pem")
|
|||||||
|
|
||||||
# long running stats file
|
# long running stats file
|
||||||
STATS_FILE = os.path.join(SCRIPT_DIR, ".long_running_stats")
|
STATS_FILE = os.path.join(SCRIPT_DIR, ".long_running_stats")
|
||||||
SMALL_DATASET[-1]["options"].extend(["--stats-file", STATS_FILE])
|
|
||||||
LARGE_DATASET[-1]["options"].extend(["--stats-file", STATS_FILE])
|
|
||||||
|
|
||||||
# get number of threads
|
# get number of threads
|
||||||
if "THREADS" in os.environ:
|
THREADS = os.environ["THREADS"] if "THREADS" in os.environ else multiprocessing.cpu_count()
|
||||||
THREADS = os.environ["THREADS"]
|
|
||||||
else:
|
|
||||||
THREADS = multiprocessing.cpu_count()
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_server(port, delay=0.1):
|
def generate_temporary_ssl_certs():
|
||||||
cmd = ["nc", "-z", "-w", "1", "127.0.0.1", str(port)]
|
|
||||||
while subprocess.call(cmd) != 0:
|
|
||||||
time.sleep(0.01)
|
|
||||||
time.sleep(delay)
|
|
||||||
|
|
||||||
|
|
||||||
# run test helper function
|
|
||||||
def run_test(args, test, options, timeout):
|
|
||||||
print("Running test '{}'".format(test))
|
|
||||||
|
|
||||||
# find binary
|
|
||||||
if test.endswith(".py"):
|
|
||||||
logging = "DEBUG" if args.verbose else "WARNING"
|
|
||||||
binary = [args.python, "-u", os.path.join(SCRIPT_DIR, test), "--logging", logging]
|
|
||||||
elif test.endswith(".cpp"):
|
|
||||||
exe = os.path.join(BUILD_DIR, "tests", "stress", test[:-4])
|
|
||||||
binary = [exe]
|
|
||||||
else:
|
|
||||||
raise Exception("Test '{}' binary not supported!".format(test))
|
|
||||||
|
|
||||||
# start test
|
|
||||||
cmd = binary + ["--worker-count", str(THREADS)] + options
|
|
||||||
start = time.time()
|
|
||||||
ret_test = subprocess.run(cmd, cwd=SCRIPT_DIR, timeout=timeout * 60)
|
|
||||||
|
|
||||||
if ret_test.returncode != 0:
|
|
||||||
raise Exception("Test '{}' binary returned non-zero ({})!".format(test, ret_test.returncode))
|
|
||||||
|
|
||||||
runtime = time.time() - start
|
|
||||||
print(" Done after {:.3f} seconds".format(runtime))
|
|
||||||
|
|
||||||
return runtime
|
|
||||||
|
|
||||||
|
|
||||||
# parse arguments
|
|
||||||
parser = argparse.ArgumentParser(description="Run stress tests on Memgraph.")
|
|
||||||
parser.add_argument("--memgraph", default=os.path.join(BUILD_DIR, "memgraph"))
|
|
||||||
parser.add_argument("--log-file", default="")
|
|
||||||
parser.add_argument("--data-directory", default="")
|
|
||||||
parser.add_argument("--python", default=os.path.join(SCRIPT_DIR, "ve3", "bin", "python3"), type=str)
|
|
||||||
parser.add_argument("--large-dataset", action="store_const", const=True, default=False)
|
|
||||||
parser.add_argument("--use-ssl", action="store_const", const=True, default=False)
|
|
||||||
parser.add_argument("--verbose", action="store_const", const=True, default=False)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# generate temporary SSL certs
|
|
||||||
if args.use_ssl:
|
|
||||||
# https://unix.stackexchange.com/questions/104171/create-ssl-certificate-non-interactively
|
# https://unix.stackexchange.com/questions/104171/create-ssl-certificate-non-interactively
|
||||||
subj = "/C=HR/ST=Zagreb/L=Zagreb/O=Memgraph/CN=db.memgraph.com"
|
subj = "/C=HR/ST=Zagreb/L=Zagreb/O=Memgraph/CN=db.memgraph.com"
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
@ -168,70 +51,184 @@ if args.use_ssl:
|
|||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# start memgraph
|
|
||||||
cwd = os.path.dirname(args.memgraph)
|
|
||||||
cmd = [
|
|
||||||
args.memgraph,
|
|
||||||
"--bolt-num-workers=" + str(THREADS),
|
|
||||||
"--storage-properties-on-edges=true",
|
|
||||||
"--storage-snapshot-on-exit=true",
|
|
||||||
"--storage-snapshot-interval-sec=600",
|
|
||||||
"--storage-snapshot-retention-count=1",
|
|
||||||
"--storage-wal-enabled=true",
|
|
||||||
"--storage-recover-on-startup=false",
|
|
||||||
"--query-execution-timeout-sec=1200",
|
|
||||||
]
|
|
||||||
if not args.verbose:
|
|
||||||
cmd += ["--log-level", "WARNING"]
|
|
||||||
if args.log_file:
|
|
||||||
cmd += ["--log-file", args.log_file]
|
|
||||||
if args.data_directory:
|
|
||||||
cmd += ["--data-directory", args.data_directory]
|
|
||||||
if args.use_ssl:
|
|
||||||
cmd += ["--bolt-cert-file", CERT_FILE, "--bolt-key-file", KEY_FILE]
|
|
||||||
proc_mg = subprocess.Popen(cmd, cwd=cwd)
|
|
||||||
wait_for_server(7687)
|
|
||||||
assert proc_mg.poll() is None, "The database binary died prematurely!"
|
|
||||||
|
|
||||||
# at exit cleanup
|
def remove_certificates() -> None:
|
||||||
@atexit.register
|
|
||||||
def cleanup():
|
|
||||||
global proc_mg
|
|
||||||
if proc_mg.poll() != None:
|
|
||||||
return
|
|
||||||
proc_mg.kill()
|
|
||||||
proc_mg.wait()
|
|
||||||
|
|
||||||
|
|
||||||
# run tests
|
|
||||||
runtimes = {}
|
|
||||||
dataset = LARGE_DATASET if args.large_dataset else SMALL_DATASET
|
|
||||||
for test in dataset:
|
|
||||||
if args.use_ssl:
|
|
||||||
test["options"] += ["--use-ssl"]
|
|
||||||
runtime = run_test(args, **test)
|
|
||||||
runtimes[os.path.splitext(test["test"])[0]] = runtime
|
|
||||||
|
|
||||||
# stop memgraph
|
|
||||||
proc_mg.terminate()
|
|
||||||
ret_mg = proc_mg.wait()
|
|
||||||
if ret_mg != 0:
|
|
||||||
raise Exception("Memgraph binary returned non-zero ({})!".format(ret_mg))
|
|
||||||
|
|
||||||
# cleanup certificates
|
|
||||||
if args.use_ssl:
|
|
||||||
os.remove(KEY_FILE)
|
os.remove(KEY_FILE)
|
||||||
os.remove(CERT_FILE)
|
os.remove(CERT_FILE)
|
||||||
|
|
||||||
# measurements
|
|
||||||
measurements = ""
|
|
||||||
for key, value in runtimes.items():
|
|
||||||
measurements += "{}.runtime {}\n".format(key, value)
|
|
||||||
with open(STATS_FILE) as f:
|
|
||||||
stats = f.read().split("\n")
|
|
||||||
measurements += "long_running.queries.executed {}\n".format(stats[0])
|
|
||||||
measurements += "long_running.queries.failed {}\n".format(stats[1])
|
|
||||||
with open(MEASUREMENTS_FILE, "w") as f:
|
|
||||||
f.write(measurements)
|
|
||||||
|
|
||||||
print("Done!")
|
def parse_arguments() -> Args:
|
||||||
|
# parse arguments
|
||||||
|
parser = argparse.ArgumentParser(description="Run stress tests on Memgraph.")
|
||||||
|
parser.add_argument("--memgraph", default=os.path.join(BUILD_DIR, "memgraph"))
|
||||||
|
parser.add_argument("--log-file", default="")
|
||||||
|
parser.add_argument("--data-directory", default="")
|
||||||
|
parser.add_argument("--python", default=os.path.join(SCRIPT_DIR, "ve3", "bin", "python3"), type=str)
|
||||||
|
parser.add_argument("--large-dataset", action="store_const", const=True, default=False)
|
||||||
|
parser.add_argument("--small-dataset", action="store_const", const=True, default=False)
|
||||||
|
parser.add_argument("--specific-test", default="")
|
||||||
|
parser.add_argument("--use-ssl", action="store_const", const=True, default=False)
|
||||||
|
parser.add_argument("--verbose", action="store_const", const=True, default=False)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_server(port, delay=0.1) -> None:
|
||||||
|
cmd = ["nc", "-z", "-w", "1", "127.0.0.1", str(port)]
|
||||||
|
while subprocess.call(cmd) != 0:
|
||||||
|
time.sleep(0.01)
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
|
||||||
|
def start_memgraph(args: Args) -> Popen:
|
||||||
|
"""Starts Memgraph and return the process"""
|
||||||
|
cwd = os.path.dirname(args.memgraph)
|
||||||
|
cmd = [
|
||||||
|
args.memgraph,
|
||||||
|
"--bolt-num-workers=" + str(THREADS),
|
||||||
|
"--storage-properties-on-edges=true",
|
||||||
|
"--storage-snapshot-on-exit=true",
|
||||||
|
"--storage-snapshot-interval-sec=600",
|
||||||
|
"--storage-snapshot-retention-count=1",
|
||||||
|
"--storage-wal-enabled=true",
|
||||||
|
"--storage-recover-on-startup=false",
|
||||||
|
"--query-execution-timeout-sec=1200",
|
||||||
|
"--bolt-server-name-for-init=Neo4j/",
|
||||||
|
]
|
||||||
|
if not args.verbose:
|
||||||
|
cmd += ["--log-level", "WARNING"]
|
||||||
|
if args.log_file:
|
||||||
|
cmd += ["--log-file", args.log_file]
|
||||||
|
if args.data_directory:
|
||||||
|
cmd += ["--data-directory", args.data_directory]
|
||||||
|
if args.use_ssl:
|
||||||
|
cmd += ["--bolt-cert-file", CERT_FILE, "--bolt-key-file", KEY_FILE]
|
||||||
|
memgraph_proc = subprocess.Popen(cmd, cwd=cwd)
|
||||||
|
wait_for_server(7687)
|
||||||
|
|
||||||
|
assert memgraph_proc.poll() is None, "The database binary died prematurely!"
|
||||||
|
|
||||||
|
return memgraph_proc
|
||||||
|
|
||||||
|
|
||||||
|
def stop_memgraph(proc_mg: Popen) -> None:
|
||||||
|
proc_mg.terminate()
|
||||||
|
ret_mg = proc_mg.wait()
|
||||||
|
if ret_mg != 0:
|
||||||
|
raise Exception("Memgraph binary returned non-zero ({})!".format(ret_mg))
|
||||||
|
|
||||||
|
|
||||||
|
def run_test(args: Args, test: str, options: List[str], timeout: int, mode: DatabaseMode) -> float:
|
||||||
|
"""Runs tests for a set of specific database configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Arguments passed to the test
|
||||||
|
test: Test name
|
||||||
|
options: List of options specific for each test
|
||||||
|
timeout: Timeout in minutes
|
||||||
|
mode: DatabaseMode (storage mode & isolation level pair)
|
||||||
|
"""
|
||||||
|
print("Running test '{}'".format(test))
|
||||||
|
|
||||||
|
binary = _find_test_binary(args, test)
|
||||||
|
|
||||||
|
# start test
|
||||||
|
cmd = (
|
||||||
|
binary
|
||||||
|
+ [
|
||||||
|
"--worker-count",
|
||||||
|
str(THREADS),
|
||||||
|
"--isolation-level",
|
||||||
|
mode.isolation_level,
|
||||||
|
"--storage-mode",
|
||||||
|
mode.storage_mode,
|
||||||
|
]
|
||||||
|
+ options
|
||||||
|
)
|
||||||
|
start = time.time()
|
||||||
|
ret_test = subprocess.run(cmd, cwd=SCRIPT_DIR, timeout=timeout * 60)
|
||||||
|
|
||||||
|
if ret_test.returncode != 0:
|
||||||
|
raise Exception("Test '{}' binary returned non-zero ({})!".format(test, ret_test.returncode))
|
||||||
|
|
||||||
|
runtime = time.time() - start
|
||||||
|
print(" Done after {:.3f} seconds".format(runtime))
|
||||||
|
|
||||||
|
return runtime
|
||||||
|
|
||||||
|
|
||||||
|
def _find_test_binary(args: Args, test: str) -> List[str]:
|
||||||
|
if test.endswith(".py"):
|
||||||
|
logging = "DEBUG" if args.verbose else "WARNING"
|
||||||
|
return [args.python, "-u", os.path.join(SCRIPT_DIR, test), "--logging", logging]
|
||||||
|
|
||||||
|
if test.endswith(".cpp"):
|
||||||
|
exe = os.path.join(BUILD_DIR, "tests", "stress", test[:-4])
|
||||||
|
return [exe]
|
||||||
|
|
||||||
|
raise Exception("Test '{}' binary not supported!".format(test))
|
||||||
|
|
||||||
|
|
||||||
|
def run_stress_test_suite(args: Args) -> Dict[str, float]:
|
||||||
|
def cleanup(memgraph_proc):
|
||||||
|
if memgraph_proc.poll() != None:
|
||||||
|
return
|
||||||
|
memgraph_proc.kill()
|
||||||
|
memgraph_proc.wait()
|
||||||
|
|
||||||
|
runtimes = {}
|
||||||
|
|
||||||
|
if args.large_dataset and args.small_dataset:
|
||||||
|
raise Exception("Choose only a large dataset or a small dataset for stress testing!")
|
||||||
|
|
||||||
|
dataset = LARGE_DATASET if args.large_dataset else SMALL_DATASET
|
||||||
|
if args.specific_test:
|
||||||
|
dataset = [x for x in dataset if x[DatasetConstants.TEST] == args.specific_test]
|
||||||
|
if not len(dataset):
|
||||||
|
raise Exception("Specific dataset is not found for stress testing!")
|
||||||
|
|
||||||
|
for test in dataset:
|
||||||
|
if args.use_ssl:
|
||||||
|
test[DatasetConstants.OPTIONS] += ["--use-ssl"]
|
||||||
|
|
||||||
|
for mode in test[DatasetConstants.MODE]:
|
||||||
|
test_run = copy.deepcopy(test)
|
||||||
|
|
||||||
|
# Run for every specified combination of storage mode, serialization mode, etc (if extended)
|
||||||
|
test_run[DatasetConstants.MODE] = mode
|
||||||
|
|
||||||
|
memgraph_proc = start_memgraph(args)
|
||||||
|
runtime = run_test(args, **test_run)
|
||||||
|
runtimes[os.path.splitext(test[DatasetConstants.TEST])[0]] = runtime
|
||||||
|
|
||||||
|
stop_memgraph(memgraph_proc)
|
||||||
|
cleanup(memgraph_proc)
|
||||||
|
|
||||||
|
return runtimes
|
||||||
|
|
||||||
|
|
||||||
|
def write_stats(runtimes: Dict[str, float]) -> None:
|
||||||
|
measurements = ""
|
||||||
|
for key, value in runtimes.items():
|
||||||
|
measurements += "{}.runtime {}\n".format(key, value)
|
||||||
|
with open(STATS_FILE) as f:
|
||||||
|
stats = f.read().split("\n")
|
||||||
|
measurements += "long_running.queries.executed {}\n".format(stats[0])
|
||||||
|
measurements += "long_running.queries.failed {}\n".format(stats[1])
|
||||||
|
with open(MEASUREMENTS_FILE, "w") as f:
|
||||||
|
f.write(measurements)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = parse_arguments()
|
||||||
|
|
||||||
|
if args.use_ssl:
|
||||||
|
generate_temporary_ssl_certs()
|
||||||
|
|
||||||
|
runtimes = run_stress_test_suite(args)
|
||||||
|
|
||||||
|
if args.use_ssl:
|
||||||
|
remove_certificates()
|
||||||
|
|
||||||
|
write_stats(runtimes)
|
||||||
|
|
||||||
|
print("Successfully ran stress tests!")
|
||||||
|
@ -12,44 +12,48 @@
|
|||||||
# by the Apache License, Version 2.0, included in the file
|
# by the Apache License, Version 2.0, included in the file
|
||||||
# licenses/APL.txt.
|
# licenses/APL.txt.
|
||||||
|
|
||||||
'''
|
"""
|
||||||
Large scale stress test. Tests only node creation.
|
Large scale stress test. Tests only node creation.
|
||||||
|
|
||||||
The idea is to run this test on machines with huge amount of memory e.g. 2TB.
|
The idea is to run this test on machines with huge amount of memory e.g. 2TB.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from common import connection_argument_parser, argument_session
|
|
||||||
|
from common import argument_session, connection_argument_parser
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
'''
|
"""
|
||||||
Parses user arguments
|
Parses user arguments
|
||||||
|
|
||||||
:return: parsed arguments
|
:return: parsed arguments
|
||||||
'''
|
"""
|
||||||
parser = connection_argument_parser()
|
parser = connection_argument_parser()
|
||||||
|
|
||||||
# specific
|
# specific
|
||||||
parser.add_argument('--worker-count', type=int,
|
parser.add_argument(
|
||||||
default=multiprocessing.cpu_count(),
|
"--worker-count", type=int, default=multiprocessing.cpu_count(), help="Number of concurrent workers."
|
||||||
help='Number of concurrent workers.')
|
)
|
||||||
parser.add_argument("--logging", default="INFO",
|
parser.add_argument(
|
||||||
choices=["INFO", "DEBUG", "WARNING", "ERROR"],
|
"--logging", default="INFO", choices=["INFO", "DEBUG", "WARNING", "ERROR"], help="Logging level"
|
||||||
help="Logging level")
|
)
|
||||||
parser.add_argument('--vertex-count', type=int, default=100,
|
parser.add_argument("--vertex-count", type=int, default=100, help="Number of created vertices.")
|
||||||
help='Number of created vertices.')
|
parser.add_argument(
|
||||||
parser.add_argument('--max-property-value', type=int, default=1000,
|
"--max-property-value",
|
||||||
help='Maximum value of property - 1. A created node '
|
type=int,
|
||||||
'will have a property with random value from 0 to '
|
default=1000,
|
||||||
'max_property_value - 1.')
|
help="Maximum value of property - 1. A created node "
|
||||||
parser.add_argument('--create-pack-size', type=int, default=1,
|
"will have a property with random value from 0 to "
|
||||||
help='Number of CREATE clauses in a query')
|
"max_property_value - 1.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--create-pack-size", type=int, default=1, help="Number of CREATE clauses in a query")
|
||||||
|
parser.add_argument("--isolation-level", type=str, required=True, help="Database isolation level.")
|
||||||
|
parser.add_argument("--storage-mode", type=str, required=True, help="Database storage mode.")
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@ -58,51 +62,46 @@ args = parse_args()
|
|||||||
|
|
||||||
|
|
||||||
def create_worker(worker_id):
|
def create_worker(worker_id):
|
||||||
'''
|
"""
|
||||||
Creates nodes and checks that all nodes were created.
|
Creates nodes and checks that all nodes were created.
|
||||||
|
|
||||||
:param worker_id: worker id
|
:param worker_id: worker id
|
||||||
|
|
||||||
:return: tuple (worker_id, create execution time, time unit)
|
:return: tuple (worker_id, create execution time, time unit)
|
||||||
'''
|
"""
|
||||||
assert args.vertex_count > 0, 'Number of vertices has to be positive int'
|
assert args.vertex_count > 0, "Number of vertices has to be positive int"
|
||||||
|
|
||||||
generated_xs = defaultdict(int)
|
generated_xs = defaultdict(int)
|
||||||
create_query = ''
|
create_query = ""
|
||||||
with argument_session(args) as session:
|
with argument_session(args) as session:
|
||||||
# create vertices
|
# create vertices
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
for i in range(0, args.vertex_count):
|
for i in range(0, args.vertex_count):
|
||||||
random_number = random.randint(0, args.max_property_value - 1)
|
random_number = random.randint(0, args.max_property_value - 1)
|
||||||
generated_xs[random_number] += 1
|
generated_xs[random_number] += 1
|
||||||
create_query += 'CREATE (:Label_T%s {x: %s}) ' % \
|
create_query += "CREATE (:Label_T%s {x: %s}) " % (worker_id, random_number)
|
||||||
(worker_id, random_number)
|
|
||||||
# if full back or last item -> execute query
|
# if full back or last item -> execute query
|
||||||
if (i + 1) % args.create_pack_size == 0 or \
|
if (i + 1) % args.create_pack_size == 0 or i == args.vertex_count - 1:
|
||||||
i == args.vertex_count - 1:
|
|
||||||
session.run(create_query).consume()
|
session.run(create_query).consume()
|
||||||
create_query = ''
|
create_query = ""
|
||||||
create_time = time.time()
|
create_time = time.time()
|
||||||
# check total count
|
# check total count
|
||||||
result_set = session.run('MATCH (n:Label_T%s) RETURN count(n) AS cnt' %
|
result_set = session.run("MATCH (n:Label_T%s) RETURN count(n) AS cnt" % worker_id).data()[0]
|
||||||
worker_id).data()[0]
|
assert result_set["cnt"] == args.vertex_count, "Create vertices Expected: %s Created: %s" % (
|
||||||
assert result_set['cnt'] == args.vertex_count, \
|
args.vertex_count,
|
||||||
'Create vertices Expected: %s Created: %s' % \
|
result_set["cnt"],
|
||||||
(args.vertex_count, result_set['cnt'])
|
)
|
||||||
# check count per property value
|
# check count per property value
|
||||||
for i, size in generated_xs.items():
|
for i, size in generated_xs.items():
|
||||||
result_set = session.run('MATCH (n:Label_T%s {x: %s}) '
|
result_set = session.run("MATCH (n:Label_T%s {x: %s}) " "RETURN count(n) AS cnt" % (worker_id, i)).data()[0]
|
||||||
'RETURN count(n) AS cnt'
|
assert result_set["cnt"] == size, "Per x count isn't good " "(Label: Label_T%s, prop x: %s" % (worker_id, i)
|
||||||
% (worker_id, i)).data()[0]
|
|
||||||
assert result_set['cnt'] == size, "Per x count isn't good " \
|
|
||||||
"(Label: Label_T%s, prop x: %s" % (worker_id, i)
|
|
||||||
return (worker_id, create_time - start_time, "s")
|
return (worker_id, create_time - start_time, "s")
|
||||||
|
|
||||||
|
|
||||||
def create_handler():
|
def create_handler():
|
||||||
'''
|
"""
|
||||||
Initializes processes and starts the execution.
|
Initializes processes and starts the execution.
|
||||||
'''
|
"""
|
||||||
# instance cleanup
|
# instance cleanup
|
||||||
with argument_session(args) as session:
|
with argument_session(args) as session:
|
||||||
session.run("MATCH (n) DETACH DELETE n").consume()
|
session.run("MATCH (n) DETACH DELETE n").consume()
|
||||||
@ -113,21 +112,19 @@ def create_handler():
|
|||||||
|
|
||||||
# concurrent create execution & tests
|
# concurrent create execution & tests
|
||||||
with multiprocessing.Pool(args.worker_count) as p:
|
with multiprocessing.Pool(args.worker_count) as p:
|
||||||
for worker_id, create_time, time_unit in \
|
for worker_id, create_time, time_unit in p.map(create_worker, [i for i in range(args.worker_count)]):
|
||||||
p.map(create_worker, [i for i in range(args.worker_count)]):
|
log.info("Worker ID: %s; Create time: %s%s" % (worker_id, create_time, time_unit))
|
||||||
log.info('Worker ID: %s; Create time: %s%s' %
|
|
||||||
(worker_id, create_time, time_unit))
|
|
||||||
|
|
||||||
# check total count
|
# check total count
|
||||||
expected_total_count = args.worker_count * args.vertex_count
|
expected_total_count = args.worker_count * args.vertex_count
|
||||||
total_count = session.run(
|
total_count = session.run("MATCH (n) RETURN count(n) AS cnt").data()[0]["cnt"]
|
||||||
'MATCH (n) RETURN count(n) AS cnt').data()[0]['cnt']
|
assert total_count == expected_total_count, "Total vertex number: %s Expected: %s" % (
|
||||||
assert total_count == expected_total_count, \
|
total_count,
|
||||||
'Total vertex number: %s Expected: %s' % \
|
expected_total_count,
|
||||||
(total_count, expected_total_count)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=args.logging)
|
logging.basicConfig(level=args.logging)
|
||||||
if args.logging != "DEBUG":
|
if args.logging != "DEBUG":
|
||||||
logging.getLogger("neo4j").setLevel(logging.WARNING)
|
logging.getLogger("neo4j").setLevel(logging.WARNING)
|
||||||
|
228
tests/stress/detach_delete.py
Normal file
228
tests/stress/detach_delete.py
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2023 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Large bipartite graph stress test.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from argparse import Namespace as Args
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Callable, Tuple
|
||||||
|
|
||||||
|
from common import OutputData, connection_argument_parser, get_memgraph
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
output_data = OutputData()
|
||||||
|
|
||||||
|
NUMBER_NODES_IN_CHAIN = 4
|
||||||
|
CREATE_FUNCTION = "CREATE"
|
||||||
|
DELETE_FUNCTION = "DELETE"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> Args:
|
||||||
|
"""
|
||||||
|
Parses user arguments
|
||||||
|
|
||||||
|
:return: parsed arguments
|
||||||
|
"""
|
||||||
|
parser = connection_argument_parser()
|
||||||
|
parser.add_argument("--worker-count", type=int, default=4, help="Number of concurrent workers.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--logging", default="INFO", choices=["INFO", "DEBUG", "WARNING", "ERROR"], help="Logging level"
|
||||||
|
)
|
||||||
|
parser.add_argument("--repetition-count", type=int, default=1000, help="Number of times to perform the action")
|
||||||
|
parser.add_argument("--isolation-level", type=str, required=True, help="Database isolation level.")
|
||||||
|
parser.add_argument("--storage-mode", type=str, required=True, help="Database storage mode.")
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Worker:
|
||||||
|
"""
|
||||||
|
Class that performs a function defined in the `type` argument
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type - either `CREATE` or `DELETE`, signifying the function that's going to be performed
|
||||||
|
by the worker
|
||||||
|
id - worker id
|
||||||
|
total_worker_cnt - total number of workers for reference
|
||||||
|
repetition_count - number of times to perform the worker action
|
||||||
|
sleep_sec - float for subsecond sleeping between two subsequent actions
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: str
|
||||||
|
id: int
|
||||||
|
total_worker_cnt: int
|
||||||
|
repetition_count: int
|
||||||
|
sleep_sec: float
|
||||||
|
|
||||||
|
|
||||||
|
def timed_function(name) -> Callable:
|
||||||
|
"""
|
||||||
|
Times performed function
|
||||||
|
"""
|
||||||
|
|
||||||
|
def actual_decorator(func) -> Callable:
|
||||||
|
@wraps(func)
|
||||||
|
def timed_wrapper(*args, **kwargs) -> Any:
|
||||||
|
start_time = time.time()
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
end_time = time.time()
|
||||||
|
output_data.add_measurement(name, end_time - start_time)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return timed_wrapper
|
||||||
|
|
||||||
|
return actual_decorator
|
||||||
|
|
||||||
|
|
||||||
|
@timed_function("cleanup_time")
|
||||||
|
def clean_database() -> None:
|
||||||
|
memgraph = get_memgraph(args)
|
||||||
|
memgraph.execute("MATCH (n) DETACH DELETE n")
|
||||||
|
|
||||||
|
|
||||||
|
def create_indices() -> None:
|
||||||
|
memgraph = get_memgraph(args)
|
||||||
|
memgraph.execute("CREATE INDEX ON :Node")
|
||||||
|
memgraph.execute("CREATE INDEX ON :Node(id)")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_database_mode() -> None:
|
||||||
|
memgraph = get_memgraph(args)
|
||||||
|
memgraph.execute(f"STORAGE MODE {args.storage_mode}")
|
||||||
|
memgraph.execute(f"SET GLOBAL TRANSACTION ISOLATION LEVEL {args.isolation_level}")
|
||||||
|
|
||||||
|
|
||||||
|
def execute_function(worker: Worker) -> Worker:
|
||||||
|
"""
|
||||||
|
Executes the function based on the worker type
|
||||||
|
"""
|
||||||
|
if worker.type == CREATE_FUNCTION:
|
||||||
|
run_writer(worker.total_worker_cnt, worker.repetition_count, worker.sleep_sec, worker.id)
|
||||||
|
return worker
|
||||||
|
|
||||||
|
if worker.type == DELETE_FUNCTION:
|
||||||
|
run_deleter(worker.total_worker_cnt, worker.repetition_count, worker.sleep_sec)
|
||||||
|
return worker
|
||||||
|
|
||||||
|
raise Exception("Worker function not recognized, raising exception!")
|
||||||
|
|
||||||
|
|
||||||
|
def run_writer(total_workers_cnt: int, repetition_count: int, sleep_sec: float, worker_id: int) -> int:
|
||||||
|
"""
|
||||||
|
This writer creates a chain and wants to verify after each action if the action he performed is
|
||||||
|
a valid graph. A graph is valid if the number of nodes is preserved, and the chain is either
|
||||||
|
not present or present completely.
|
||||||
|
"""
|
||||||
|
memgraph = get_memgraph(args)
|
||||||
|
|
||||||
|
def create():
|
||||||
|
try:
|
||||||
|
memgraph.execute(
|
||||||
|
f"MERGE (:Node{worker_id} {{id: 1}})-[:REL]-(:Node{worker_id} {{id: 2}})-[:REL]-(:Node{worker_id} {{id: 3}})-[:REL]-(:Node{worker_id} {{id: 4}})"
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def verify() -> Tuple[bool, int]:
|
||||||
|
# We always create X nodes and therefore the number of nodes needs to be always a fraction of X
|
||||||
|
count = list(memgraph.execute_and_fetch(f"MATCH (n) RETURN COUNT(n) AS cnt"))[0]["cnt"]
|
||||||
|
log.info(f"Worker {worker_id} verified graph count {count} in repetition {curr_repetition}")
|
||||||
|
|
||||||
|
assert count <= total_workers_cnt * NUMBER_NODES_IN_CHAIN and count % NUMBER_NODES_IN_CHAIN == 0
|
||||||
|
|
||||||
|
ids = list(
|
||||||
|
memgraph.execute_and_fetch(
|
||||||
|
f"MATCH (n:Node{worker_id} {{id: 1}})-->(m)-->(o)-->(p) RETURN n.id AS id1, m.id AS id2, o.id AS id3, p.id AS id4"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(ids):
|
||||||
|
result = ids[0]
|
||||||
|
assert "id1" in result and "id2" in result and "id3" in result and "id4" in result
|
||||||
|
assert result["id1"] == 1 and result["id2"] == 2 and result["id3"] == 3 and result["id4"] == 4
|
||||||
|
log.info(f"Worker {worker_id} verified graph chain is valid in repetition {curr_repetition}")
|
||||||
|
else:
|
||||||
|
log.info(f"Worker {worker_id} does not have a chain in repetition {repetition_count}")
|
||||||
|
|
||||||
|
curr_repetition = 0
|
||||||
|
|
||||||
|
while curr_repetition < repetition_count:
|
||||||
|
log.info(f"Worker {worker_id} started iteration {curr_repetition}")
|
||||||
|
create()
|
||||||
|
time.sleep(sleep_sec)
|
||||||
|
log.info(f"Worker {worker_id} created chain in iteration {curr_repetition}")
|
||||||
|
|
||||||
|
verify()
|
||||||
|
|
||||||
|
curr_repetition += 1
|
||||||
|
|
||||||
|
|
||||||
|
def run_deleter(total_workers_cnt: int, repetition_count: int, sleep_sec: float) -> None:
|
||||||
|
"""
|
||||||
|
Periodic deletion of an arbitrary chain in the graph
|
||||||
|
"""
|
||||||
|
memgraph = get_memgraph(args)
|
||||||
|
|
||||||
|
def delete_part_of_graph(id: int):
|
||||||
|
try:
|
||||||
|
memgraph.execute(f"MATCH (n:Node{id}) DETACH DELETE n")
|
||||||
|
log.info(f"Worker deleted chain with nodes of id {id}")
|
||||||
|
except Exception as ex:
|
||||||
|
log.info(f"Worker failed to delete the chain with id {id}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
curr_repetition = 0
|
||||||
|
while curr_repetition < repetition_count:
|
||||||
|
random_part_of_graph = random.randint(0, total_workers_cnt - 1)
|
||||||
|
delete_part_of_graph(random_part_of_graph)
|
||||||
|
time.sleep(sleep_sec)
|
||||||
|
curr_repetition += 1
|
||||||
|
|
||||||
|
|
||||||
|
@timed_function("total_execution_time")
|
||||||
|
def execution_handler() -> None:
|
||||||
|
clean_database()
|
||||||
|
log.info("Database is clean.")
|
||||||
|
|
||||||
|
setup_database_mode()
|
||||||
|
|
||||||
|
create_indices()
|
||||||
|
|
||||||
|
worker_count = args.worker_count
|
||||||
|
rep_count = args.repetition_count
|
||||||
|
|
||||||
|
workers = [Worker(CREATE_FUNCTION, x, worker_count - 1, rep_count, 0.2) for x in range(worker_count - 1)]
|
||||||
|
workers.append(Worker(DELETE_FUNCTION, -1, worker_count - 1, rep_count, 0.15))
|
||||||
|
|
||||||
|
with multiprocessing.Pool(processes=args.worker_count) as p:
|
||||||
|
for worker in p.map(execute_function, workers):
|
||||||
|
print(f"Worker {worker.type} finished!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=args.logging)
|
||||||
|
execution_handler()
|
||||||
|
if args.logging in ["DEBUG", "INFO"]:
|
||||||
|
output_data.dump()
|
@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
'''
|
"""
|
||||||
Durability Stress Test
|
Durability Stress Test
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# TODO (mg_durability_stress_test): extend once we add full durability mode
|
# TODO (mg_durability_stress_test): extend once we add full durability mode
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ import os
|
|||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
from multiprocessing import Manager, Pool
|
||||||
|
|
||||||
from common import connection_argument_parser, SessionCache
|
from common import SessionCache, connection_argument_parser
|
||||||
from multiprocessing import Pool, Manager
|
|
||||||
|
|
||||||
# Constants and args
|
# Constants and args
|
||||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
@ -35,11 +35,9 @@ SNAPSHOT_CYCLE_SEC = 5
|
|||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
parser = connection_argument_parser()
|
parser = connection_argument_parser()
|
||||||
|
|
||||||
parser.add_argument("--memgraph", default=os.path.join(BUILD_DIR,
|
parser.add_argument("--memgraph", default=os.path.join(BUILD_DIR, "memgraph"))
|
||||||
"memgraph"))
|
|
||||||
parser.add_argument("--log-file", default="")
|
parser.add_argument("--log-file", default="")
|
||||||
parser.add_argument("--verbose", action="store_const",
|
parser.add_argument("--verbose", action="store_const", const=True, default=False)
|
||||||
const=True, default=False)
|
|
||||||
parser.add_argument("--data-directory", default=DATA_DIR)
|
parser.add_argument("--data-directory", default=DATA_DIR)
|
||||||
parser.add_argument("--num-clients", default=multiprocessing.cpu_count())
|
parser.add_argument("--num-clients", default=multiprocessing.cpu_count())
|
||||||
parser.add_argument("--num-steps", type=int, default=5)
|
parser.add_argument("--num-steps", type=int, default=5)
|
||||||
@ -47,14 +45,18 @@ args = parser.parse_args()
|
|||||||
|
|
||||||
# Memgraph run command construction
|
# Memgraph run command construction
|
||||||
cwd = os.path.dirname(args.memgraph)
|
cwd = os.path.dirname(args.memgraph)
|
||||||
cmd = [args.memgraph, "--bolt-num-workers=" + str(DB_WORKERS),
|
cmd = [
|
||||||
"--storage-properties-on-edges=true",
|
args.memgraph,
|
||||||
"--storage-snapshot-on-exit=false",
|
"--bolt-num-workers=" + str(DB_WORKERS),
|
||||||
"--storage-snapshot-interval-sec=5",
|
"--storage-properties-on-edges=true",
|
||||||
"--storage-snapshot-retention-count=2",
|
"--storage-snapshot-on-exit=false",
|
||||||
"--storage-wal-enabled=true",
|
"--storage-snapshot-interval-sec=5",
|
||||||
"--storage-recover-on-startup=true",
|
"--storage-snapshot-retention-count=2",
|
||||||
"--query-execution-timeout-sec=600"]
|
"--storage-wal-enabled=true",
|
||||||
|
"--storage-recover-on-startup=true",
|
||||||
|
"--query-execution-timeout-sec=600",
|
||||||
|
"--bolt-server-name-for-init=Neo4j/v5.11.0 compatible graph database server - Memgraph",
|
||||||
|
]
|
||||||
if not args.verbose:
|
if not args.verbose:
|
||||||
cmd += ["--log-level", "WARNING"]
|
cmd += ["--log-level", "WARNING"]
|
||||||
if args.log_file:
|
if args.log_file:
|
||||||
@ -104,16 +106,13 @@ def run_client(id, data):
|
|||||||
counter = data[id]
|
counter = data[id]
|
||||||
|
|
||||||
# Check recovery for this client
|
# Check recovery for this client
|
||||||
num_nodes_db = session.run(
|
num_nodes_db = session.run("MATCH (n:%s) RETURN count(n) as cnt" % ("Client%d" % id)).data()[0]["cnt"]
|
||||||
"MATCH (n:%s) RETURN count(n) as cnt" %
|
|
||||||
("Client%d" % id)).data()[0]["cnt"]
|
|
||||||
print("Client%d DB: %d; ACTUAL: %d" % (id, num_nodes_db, data[id]))
|
print("Client%d DB: %d; ACTUAL: %d" % (id, num_nodes_db, data[id]))
|
||||||
|
|
||||||
# Execute a new set of write queries
|
# Execute a new set of write queries
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
session.run("CREATE (n:%s {id:%s}) RETURN n;" %
|
session.run("CREATE (n:%s {id:%s}) RETURN n;" % ("Client%d" % id, counter)).consume()
|
||||||
("Client%d" % id, counter)).consume()
|
|
||||||
counter += 1
|
counter += 1
|
||||||
if counter % 100000 == 0:
|
if counter % 100000 == 0:
|
||||||
print("Client %d executed %d" % (id, counter))
|
print("Client %d executed %d" % (id, counter))
|
||||||
@ -126,8 +125,7 @@ def run_client(id, data):
|
|||||||
|
|
||||||
def run_step():
|
def run_step():
|
||||||
with Pool(args.num_clients) as p:
|
with Pool(args.num_clients) as p:
|
||||||
p.starmap(run_client, [(id, data)
|
p.starmap(run_client, [(id, data) for id in range(1, args.num_clients + 1)])
|
||||||
for id in range(1, args.num_clients + 1)])
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -143,22 +141,19 @@ def main():
|
|||||||
# Also it makes sense to run this test for a longer period of time
|
# Also it makes sense to run this test for a longer period of time
|
||||||
# but with small --snapshot-cycle-sec to force that Memgraph is being
|
# but with small --snapshot-cycle-sec to force that Memgraph is being
|
||||||
# killed in the middle of generating the snapshot.
|
# killed in the middle of generating the snapshot.
|
||||||
time.sleep(
|
time.sleep(random.randint(2 * SNAPSHOT_CYCLE_SEC, 3 * SNAPSHOT_CYCLE_SEC))
|
||||||
random.randint(2 * SNAPSHOT_CYCLE_SEC, 3 * SNAPSHOT_CYCLE_SEC))
|
|
||||||
clean_memgraph()
|
clean_memgraph()
|
||||||
|
|
||||||
# Final check
|
# Final check
|
||||||
run_memgraph()
|
run_memgraph()
|
||||||
session = SessionCache.argument_session(args)
|
session = SessionCache.argument_session(args)
|
||||||
num_nodes_db = \
|
num_nodes_db = session.run("MATCH (n) RETURN count(n) AS cnt").data()[0]["cnt"]
|
||||||
session.run("MATCH (n) RETURN count(n) AS cnt").data()[0]["cnt"]
|
|
||||||
num_nodes = sum(data.values())
|
num_nodes = sum(data.values())
|
||||||
# Check that more than 99.9% of data is recoverable.
|
# Check that more than 99.9% of data is recoverable.
|
||||||
# NOTE: default WAL flush interval is 1ms.
|
# NOTE: default WAL flush interval is 1ms.
|
||||||
# Once the full sync durability mode is introduced,
|
# Once the full sync durability mode is introduced,
|
||||||
# this test has to be extended.
|
# this test has to be extended.
|
||||||
assert num_nodes_db > 0.999 * num_nodes, \
|
assert num_nodes_db > 0.999 * num_nodes, "Memgraph lost more than 0.001 of data!"
|
||||||
"Memgraph lost more than 0.001 of data!"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2022 Memgraph Ltd.
|
// Copyright 2023 Memgraph Ltd.
|
||||||
//
|
//
|
||||||
// Use of this software is governed by the Business Source License
|
// 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
|
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||||
@ -47,6 +47,9 @@ DEFINE_bool(global_queries, true, "If queries that modifiy globally should be ex
|
|||||||
|
|
||||||
DEFINE_string(stats_file, "", "File into which to write statistics.");
|
DEFINE_string(stats_file, "", "File into which to write statistics.");
|
||||||
|
|
||||||
|
DEFINE_string(isolation_level, "", "Database isolation level.");
|
||||||
|
DEFINE_string(storage_mode, "", "Database storage_mode.");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates a Graph and a Bolt session and provides CRUD op functions.
|
* Encapsulates a Graph and a Bolt session and provides CRUD op functions.
|
||||||
* Also defines a run-loop for a generic exectutor, and a graph state
|
* Also defines a run-loop for a generic exectutor, and a graph state
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2022 Memgraph Ltd.
|
// Copyright 2023 Memgraph Ltd.
|
||||||
//
|
//
|
||||||
// Use of this software is governed by the Business Source License
|
// 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
|
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||||
@ -28,6 +28,8 @@ DEFINE_string(password, "", "Password for the database");
|
|||||||
DEFINE_bool(use_ssl, false, "Set to true to connect with SSL to the server.");
|
DEFINE_bool(use_ssl, false, "Set to true to connect with SSL to the server.");
|
||||||
DEFINE_int32(worker_count, 1, "The number of concurrent workers executing queries against the server.");
|
DEFINE_int32(worker_count, 1, "The number of concurrent workers executing queries against the server.");
|
||||||
DEFINE_int32(per_worker_query_count, 100, "The number of queries each worker will try to execute.");
|
DEFINE_int32(per_worker_query_count, 100, "The number of queries each worker will try to execute.");
|
||||||
|
DEFINE_string(isolation_level, "", "Database isolation level.");
|
||||||
|
DEFINE_string(storage_mode, "", "Database storage_mode.");
|
||||||
|
|
||||||
auto make_client() {
|
auto make_client() {
|
||||||
mg::Client::Params params;
|
mg::Client::Params params;
|
||||||
|
@ -1 +1,2 @@
|
|||||||
neo4j-driver==4.1.1
|
neo4j-driver==4.1.1
|
||||||
|
gqlalchemy==1.3.3
|
||||||
|
179
tests/stress/test_config.py
Normal file
179
tests/stress/test_config.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import itertools
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
STATS_FILE = os.path.join(SCRIPT_DIR, ".long_running_stats")
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetConstants:
|
||||||
|
TEST = "test"
|
||||||
|
OPTIONS = "options"
|
||||||
|
TIMEOUT = "timeout"
|
||||||
|
MODE = "mode"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DatabaseMode:
|
||||||
|
storage_mode: str
|
||||||
|
isolation_level: str
|
||||||
|
|
||||||
|
|
||||||
|
class StorageModeConstants:
|
||||||
|
IN_MEMORY_TRANSACTIONAL = "IN_MEMORY_TRANSACTIONAL"
|
||||||
|
IN_MEMORY_ANALYTICAL = "IN_MEMORY_ANALYTICAL"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_list(cls) -> List[str]:
|
||||||
|
return [cls.IN_MEMORY_TRANSACTIONAL, cls.IN_MEMORY_ANALYTICAL]
|
||||||
|
|
||||||
|
|
||||||
|
class IsolationLevelConstants:
|
||||||
|
SNAPSHOT_ISOLATION = "SNAPSHOT ISOLATION"
|
||||||
|
READ_COMMITED = "READ COMMITED"
|
||||||
|
READ_UNCOMMITED = "READ UNCOMMITED"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_list(cls) -> List[str]:
|
||||||
|
return [cls.SNAPSHOT_SERIALIZATION, cls.READ_COMMITED, cls.READ_UNCOMMITED]
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_database_mode() -> DatabaseMode:
|
||||||
|
return DatabaseMode(StorageModeConstants.IN_MEMORY_TRANSACTIONAL, IsolationLevelConstants.SNAPSHOT_ISOLATION)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_database_modes() -> List[DatabaseMode]:
|
||||||
|
return [
|
||||||
|
DatabaseMode(x[0], x[1])
|
||||||
|
for x in itertools.product(StorageModeConstants.to_list(), IsolationLevelConstants.to_list())
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# dataset calibrated for running on Apollo (total 4min)
|
||||||
|
# bipartite.py runs for approx. 30s
|
||||||
|
# create_match.py runs for approx. 30s
|
||||||
|
# long_running runs for 1min
|
||||||
|
# long_running runs for 2min
|
||||||
|
SMALL_DATASET = [
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "bipartite.py",
|
||||||
|
DatasetConstants.OPTIONS: ["--u-count", "100", "--v-count", "100"],
|
||||||
|
DatasetConstants.TIMEOUT: 5,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "detach_delete.py",
|
||||||
|
DatasetConstants.OPTIONS: ["--worker-count", "4", "--repetition-count", "100"],
|
||||||
|
DatasetConstants.TIMEOUT: 5,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "create_match.py",
|
||||||
|
DatasetConstants.OPTIONS: ["--vertex-count", "40000", "--create-pack-size", "100"],
|
||||||
|
DatasetConstants.TIMEOUT: 5,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "parser.cpp",
|
||||||
|
DatasetConstants.OPTIONS: ["--per-worker-query-count", "1000"],
|
||||||
|
DatasetConstants.TIMEOUT: 5,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "long_running.cpp",
|
||||||
|
DatasetConstants.OPTIONS: [
|
||||||
|
"--vertex-count",
|
||||||
|
"1000",
|
||||||
|
"--edge-count",
|
||||||
|
"5000",
|
||||||
|
"--max-time",
|
||||||
|
"1",
|
||||||
|
"--verify",
|
||||||
|
"20",
|
||||||
|
],
|
||||||
|
DatasetConstants.TIMEOUT: 5,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "long_running.cpp",
|
||||||
|
DatasetConstants.OPTIONS: [
|
||||||
|
"--vertex-count",
|
||||||
|
"10000",
|
||||||
|
"--edge-count",
|
||||||
|
"50000",
|
||||||
|
"--max-time",
|
||||||
|
"2",
|
||||||
|
"--verify",
|
||||||
|
"30",
|
||||||
|
"--stats-file",
|
||||||
|
STATS_FILE,
|
||||||
|
],
|
||||||
|
DatasetConstants.TIMEOUT: 5,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# dataset calibrated for running on daily stress instance (total 9h)
|
||||||
|
# bipartite.py and create_match.py run for approx. 15min
|
||||||
|
# long_running runs for 5min x 6 times = 30min
|
||||||
|
# long_running runs for 8h
|
||||||
|
LARGE_DATASET = (
|
||||||
|
[
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "bipartite.py",
|
||||||
|
DatasetConstants.OPTIONS: ["--u-count", "300", "--v-count", "300"],
|
||||||
|
DatasetConstants.TIMEOUT: 30,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "detach_delete.py",
|
||||||
|
DatasetConstants.OPTIONS: ["--worker-count", "4", "--repetition-count", "300"],
|
||||||
|
DatasetConstants.TIMEOUT: 5,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "create_match.py",
|
||||||
|
DatasetConstants.OPTIONS: ["--vertex-count", "500000", "--create-pack-size", "500"],
|
||||||
|
DatasetConstants.TIMEOUT: 30,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "long_running.cpp",
|
||||||
|
DatasetConstants.OPTIONS: [
|
||||||
|
"--vertex-count",
|
||||||
|
"10000",
|
||||||
|
"--edge-count",
|
||||||
|
"40000",
|
||||||
|
"--max-time",
|
||||||
|
"5",
|
||||||
|
"--verify",
|
||||||
|
"60",
|
||||||
|
],
|
||||||
|
DatasetConstants.TIMEOUT: 16,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
* 6
|
||||||
|
+ [
|
||||||
|
{
|
||||||
|
DatasetConstants.TEST: "long_running.cpp",
|
||||||
|
DatasetConstants.OPTIONS: [
|
||||||
|
"--vertex-count",
|
||||||
|
"200000",
|
||||||
|
"--edge-count",
|
||||||
|
"1000000",
|
||||||
|
"--max-time",
|
||||||
|
"480",
|
||||||
|
"--verify",
|
||||||
|
"300",
|
||||||
|
"--stats-file",
|
||||||
|
STATS_FILE,
|
||||||
|
],
|
||||||
|
DatasetConstants.TIMEOUT: 500,
|
||||||
|
DatasetConstants.MODE: [get_default_database_mode()],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
@ -1182,6 +1182,31 @@ TYPED_TEST(InterpreterTest, ExecutionStatsValues) {
|
|||||||
ASSERT_EQ(stats["relationships-deleted"].ValueInt(), 3);
|
ASSERT_EQ(stats["relationships-deleted"].ValueInt(), 3);
|
||||||
AssertAllValuesAreZero(stats, {"nodes-deleted", "relationships-deleted"});
|
AssertAllValuesAreZero(stats, {"nodes-deleted", "relationships-deleted"});
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
auto [stream, qid] = this->Prepare("CREATE (n)-[:TO]->(m);");
|
||||||
|
this->Pull(&stream);
|
||||||
|
|
||||||
|
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||||
|
ASSERT_EQ(stats["nodes-created"].ValueInt(), 2);
|
||||||
|
ASSERT_EQ(stats["relationships-created"].ValueInt(), 1);
|
||||||
|
AssertAllValuesAreZero(stats, {"nodes-created", "relationships-created"});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto [stream, qid] = this->Prepare("MATCH (n)-[r]->(m) DELETE r;");
|
||||||
|
this->Pull(&stream);
|
||||||
|
|
||||||
|
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||||
|
ASSERT_EQ(stats["relationships-deleted"].ValueInt(), 1);
|
||||||
|
AssertAllValuesAreZero(stats, {"nodes-deleted", "relationships-deleted"});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto [stream, qid] = this->Prepare("MATCH (n) DELETE n;");
|
||||||
|
this->Pull(&stream);
|
||||||
|
|
||||||
|
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||||
|
ASSERT_EQ(stats["nodes-deleted"].ValueInt(), 2);
|
||||||
|
AssertAllValuesAreZero(stats, {"nodes-deleted", ""});
|
||||||
|
}
|
||||||
{
|
{
|
||||||
auto [stream, qid] = this->Prepare("CREATE (:L1:L2:L3), (:L1), (:L1), (:L2);");
|
auto [stream, qid] = this->Prepare("CREATE (:L1:L2:L3), (:L1), (:L1), (:L2);");
|
||||||
this->Pull(&stream);
|
this->Pull(&stream);
|
||||||
|
@ -775,6 +775,7 @@ TYPED_TEST(QueryPlanTest, Delete) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// detach delete a single vertex
|
// detach delete a single vertex
|
||||||
|
// delete will not happen as we are deleting in bulk
|
||||||
{
|
{
|
||||||
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
||||||
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
||||||
@ -783,8 +784,8 @@ TYPED_TEST(QueryPlanTest, Delete) {
|
|||||||
auto context = MakeContext(this->storage, symbol_table, &dba);
|
auto context = MakeContext(this->storage, symbol_table, &dba);
|
||||||
delete_op->MakeCursor(memgraph::utils::NewDeleteResource())->Pull(frame, context);
|
delete_op->MakeCursor(memgraph::utils::NewDeleteResource())->Pull(frame, context);
|
||||||
dba.AdvanceCommand();
|
dba.AdvanceCommand();
|
||||||
EXPECT_EQ(3, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
EXPECT_EQ(4, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
||||||
EXPECT_EQ(3, CountEdges(&dba, memgraph::storage::View::OLD));
|
EXPECT_EQ(6, CountEdges(&dba, memgraph::storage::View::OLD));
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete all remaining edges
|
// delete all remaining edges
|
||||||
@ -797,7 +798,7 @@ TYPED_TEST(QueryPlanTest, Delete) {
|
|||||||
auto context = MakeContext(this->storage, symbol_table, &dba);
|
auto context = MakeContext(this->storage, symbol_table, &dba);
|
||||||
PullAll(*delete_op, &context);
|
PullAll(*delete_op, &context);
|
||||||
dba.AdvanceCommand();
|
dba.AdvanceCommand();
|
||||||
EXPECT_EQ(3, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
EXPECT_EQ(4, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
||||||
EXPECT_EQ(0, CountEdges(&dba, memgraph::storage::View::OLD));
|
EXPECT_EQ(0, CountEdges(&dba, memgraph::storage::View::OLD));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1012,10 +1013,11 @@ TYPED_TEST(QueryPlanTest, DeleteTwiceDeleteBlockingEdge) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TYPED_TEST(QueryPlanTest, DeleteReturn) {
|
TYPED_TEST(QueryPlanTest, DeleteReturn) {
|
||||||
|
// MATCH (n) DETACH DELETE n RETURN n
|
||||||
auto storage_dba = this->db->Access();
|
auto storage_dba = this->db->Access();
|
||||||
memgraph::query::DbAccessor dba(storage_dba.get());
|
memgraph::query::DbAccessor dba(storage_dba.get());
|
||||||
|
|
||||||
// make a fully-connected (one-direction, no cycles) with 4 nodes
|
// graph with 4 vertices
|
||||||
auto prop = PROPERTY_PAIR(dba, "property");
|
auto prop = PROPERTY_PAIR(dba, "property");
|
||||||
for (int i = 0; i < 4; ++i) {
|
for (int i = 0; i < 4; ++i) {
|
||||||
auto va = dba.InsertVertex();
|
auto va = dba.InsertVertex();
|
||||||
@ -1033,10 +1035,12 @@ TYPED_TEST(QueryPlanTest, DeleteReturn) {
|
|||||||
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
||||||
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, true);
|
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, true);
|
||||||
|
|
||||||
|
auto accumulate_op = std::make_shared<plan::Accumulate>(delete_op, delete_op->ModifiedSymbols(symbol_table), true);
|
||||||
|
|
||||||
auto prop_lookup = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
auto prop_lookup = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
||||||
auto n_p =
|
auto n_p =
|
||||||
this->storage.template Create<NamedExpression>("n", prop_lookup)->MapTo(symbol_table.CreateSymbol("bla", true));
|
this->storage.template Create<NamedExpression>("n", prop_lookup)->MapTo(symbol_table.CreateSymbol("bla", true));
|
||||||
auto produce = MakeProduce(delete_op, n_p);
|
auto produce = MakeProduce(accumulate_op, n_p);
|
||||||
|
|
||||||
auto context = MakeContext(this->storage, symbol_table, &dba);
|
auto context = MakeContext(this->storage, symbol_table, &dba);
|
||||||
ASSERT_THROW(CollectProduce(*produce, &context), QueryRuntimeException);
|
ASSERT_THROW(CollectProduce(*produce, &context), QueryRuntimeException);
|
||||||
@ -1757,6 +1761,7 @@ TYPED_TEST(QueryPlanTest, RemoveLabelsOnNull) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TYPED_TEST(QueryPlanTest, DeleteSetProperty) {
|
TYPED_TEST(QueryPlanTest, DeleteSetProperty) {
|
||||||
|
// MATCH (n) DELETE n SET n.property = 42 RETURN n
|
||||||
auto storage_dba = this->db->Access();
|
auto storage_dba = this->db->Access();
|
||||||
memgraph::query::DbAccessor dba(storage_dba.get());
|
memgraph::query::DbAccessor dba(storage_dba.get());
|
||||||
// Add a single vertex.
|
// Add a single vertex.
|
||||||
@ -1764,18 +1769,25 @@ TYPED_TEST(QueryPlanTest, DeleteSetProperty) {
|
|||||||
dba.AdvanceCommand();
|
dba.AdvanceCommand();
|
||||||
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
||||||
SymbolTable symbol_table;
|
SymbolTable symbol_table;
|
||||||
// MATCH (n) DELETE n SET n.property = 42
|
|
||||||
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
||||||
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
||||||
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
||||||
auto prop = PROPERTY_PAIR(dba, "property");
|
auto prop = PROPERTY_PAIR(dba, "property");
|
||||||
auto n_prop = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
auto n_prop = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
||||||
auto set_op = std::make_shared<plan::SetProperty>(delete_op, prop.second, n_prop, LITERAL(42));
|
auto set_op = std::make_shared<plan::SetProperty>(delete_op, prop.second, n_prop, LITERAL(42));
|
||||||
|
auto accumulate_op = std::make_shared<plan::Accumulate>(set_op, set_op->ModifiedSymbols(symbol_table), true);
|
||||||
|
|
||||||
|
auto prop_lookup = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
||||||
|
auto n_p =
|
||||||
|
this->storage.template Create<NamedExpression>("n", prop_lookup)->MapTo(symbol_table.CreateSymbol("bla", true));
|
||||||
|
auto produce = MakeProduce(accumulate_op, n_p);
|
||||||
auto context = MakeContext(this->storage, symbol_table, &dba);
|
auto context = MakeContext(this->storage, symbol_table, &dba);
|
||||||
EXPECT_THROW(PullAll(*set_op, &context), QueryRuntimeException);
|
ASSERT_THROW(CollectProduce(*produce, &context), QueryRuntimeException);
|
||||||
}
|
}
|
||||||
|
|
||||||
TYPED_TEST(QueryPlanTest, DeleteSetPropertiesFromMap) {
|
TYPED_TEST(QueryPlanTest, DeleteSetPropertiesFromMap) {
|
||||||
|
// MATCH (n) DELETE n SET n = {property: 42} return n
|
||||||
|
// MATCH (n) DELETE n SET n += {property: 42} return n
|
||||||
auto storage_dba = this->db->Access();
|
auto storage_dba = this->db->Access();
|
||||||
memgraph::query::DbAccessor dba(storage_dba.get());
|
memgraph::query::DbAccessor dba(storage_dba.get());
|
||||||
// Add a single vertex.
|
// Add a single vertex.
|
||||||
@ -1783,7 +1795,6 @@ TYPED_TEST(QueryPlanTest, DeleteSetPropertiesFromMap) {
|
|||||||
dba.AdvanceCommand();
|
dba.AdvanceCommand();
|
||||||
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
||||||
SymbolTable symbol_table;
|
SymbolTable symbol_table;
|
||||||
// MATCH (n) DELETE n SET n = {property: 42}
|
|
||||||
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
||||||
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
||||||
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
||||||
@ -1793,12 +1804,18 @@ TYPED_TEST(QueryPlanTest, DeleteSetPropertiesFromMap) {
|
|||||||
auto *rhs = this->storage.template Create<MapLiteral>(prop_map);
|
auto *rhs = this->storage.template Create<MapLiteral>(prop_map);
|
||||||
for (auto op_type : {plan::SetProperties::Op::REPLACE, plan::SetProperties::Op::UPDATE}) {
|
for (auto op_type : {plan::SetProperties::Op::REPLACE, plan::SetProperties::Op::UPDATE}) {
|
||||||
auto set_op = std::make_shared<plan::SetProperties>(delete_op, n.sym_, rhs, op_type);
|
auto set_op = std::make_shared<plan::SetProperties>(delete_op, n.sym_, rhs, op_type);
|
||||||
|
auto accumulate_op = std::make_shared<plan::Accumulate>(set_op, set_op->ModifiedSymbols(symbol_table), false);
|
||||||
|
auto prop_lookup = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
||||||
|
auto n_p =
|
||||||
|
this->storage.template Create<NamedExpression>("n", prop_lookup)->MapTo(symbol_table.CreateSymbol("bla", true));
|
||||||
|
auto produce = MakeProduce(accumulate_op, n_p);
|
||||||
auto context = MakeContext(this->storage, symbol_table, &dba);
|
auto context = MakeContext(this->storage, symbol_table, &dba);
|
||||||
EXPECT_THROW(PullAll(*set_op, &context), QueryRuntimeException);
|
ASSERT_THROW(CollectProduce(*produce, &context), QueryRuntimeException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TYPED_TEST(QueryPlanTest, DeleteSetPropertiesFrom) {
|
TYPED_TEST(QueryPlanTest, DeleteSetPropertiesFrom) {
|
||||||
|
// MATCH (n) DELETE n SET n = n RETURN n
|
||||||
auto storage_dba = this->db->Access();
|
auto storage_dba = this->db->Access();
|
||||||
memgraph::query::DbAccessor dba(storage_dba.get());
|
memgraph::query::DbAccessor dba(storage_dba.get());
|
||||||
// Add a single vertex.
|
// Add a single vertex.
|
||||||
@ -1809,19 +1826,25 @@ TYPED_TEST(QueryPlanTest, DeleteSetPropertiesFrom) {
|
|||||||
dba.AdvanceCommand();
|
dba.AdvanceCommand();
|
||||||
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
||||||
SymbolTable symbol_table;
|
SymbolTable symbol_table;
|
||||||
// MATCH (n) DELETE n SET n = n
|
|
||||||
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
||||||
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
||||||
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
||||||
auto *rhs = IDENT("n")->MapTo(n.sym_);
|
auto *rhs = IDENT("n")->MapTo(n.sym_);
|
||||||
|
auto prop = PROPERTY_PAIR(dba, "property");
|
||||||
for (auto op_type : {plan::SetProperties::Op::REPLACE, plan::SetProperties::Op::UPDATE}) {
|
for (auto op_type : {plan::SetProperties::Op::REPLACE, plan::SetProperties::Op::UPDATE}) {
|
||||||
auto set_op = std::make_shared<plan::SetProperties>(delete_op, n.sym_, rhs, op_type);
|
auto set_op = std::make_shared<plan::SetProperties>(delete_op, n.sym_, rhs, op_type);
|
||||||
|
auto accumulate_op = std::make_shared<plan::Accumulate>(set_op, set_op->ModifiedSymbols(symbol_table), false);
|
||||||
|
auto prop_lookup = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
||||||
|
auto n_p =
|
||||||
|
this->storage.template Create<NamedExpression>("n", prop_lookup)->MapTo(symbol_table.CreateSymbol("bla", true));
|
||||||
|
auto produce = MakeProduce(accumulate_op, n_p);
|
||||||
auto context = MakeContext(this->storage, symbol_table, &dba);
|
auto context = MakeContext(this->storage, symbol_table, &dba);
|
||||||
EXPECT_THROW(PullAll(*set_op, &context), QueryRuntimeException);
|
ASSERT_THROW(CollectProduce(*produce, &context), QueryRuntimeException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TYPED_TEST(QueryPlanTest, DeleteRemoveLabels) {
|
TYPED_TEST(QueryPlanTest, DeleteRemoveLabels) {
|
||||||
|
// MATCH (n) DELETE n REMOVE n :label return n
|
||||||
auto storage_dba = this->db->Access();
|
auto storage_dba = this->db->Access();
|
||||||
memgraph::query::DbAccessor dba(storage_dba.get());
|
memgraph::query::DbAccessor dba(storage_dba.get());
|
||||||
// Add a single vertex.
|
// Add a single vertex.
|
||||||
@ -1829,17 +1852,24 @@ TYPED_TEST(QueryPlanTest, DeleteRemoveLabels) {
|
|||||||
dba.AdvanceCommand();
|
dba.AdvanceCommand();
|
||||||
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
||||||
SymbolTable symbol_table;
|
SymbolTable symbol_table;
|
||||||
// MATCH (n) DELETE n REMOVE n :label
|
|
||||||
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
||||||
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
||||||
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
||||||
std::vector<memgraph::storage::LabelId> labels{dba.NameToLabel("label")};
|
std::vector<memgraph::storage::LabelId> labels{dba.NameToLabel("label")};
|
||||||
auto rem_op = std::make_shared<plan::RemoveLabels>(delete_op, n.sym_, labels);
|
auto rem_op = std::make_shared<plan::RemoveLabels>(delete_op, n.sym_, labels);
|
||||||
|
auto accumulate_op = std::make_shared<plan::Accumulate>(rem_op, rem_op->ModifiedSymbols(symbol_table), true);
|
||||||
|
|
||||||
|
auto prop = PROPERTY_PAIR(dba, "property");
|
||||||
|
auto prop_lookup = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
||||||
|
auto n_p =
|
||||||
|
this->storage.template Create<NamedExpression>("n", prop_lookup)->MapTo(symbol_table.CreateSymbol("bla", true));
|
||||||
|
auto produce = MakeProduce(accumulate_op, n_p);
|
||||||
auto context = MakeContext(this->storage, symbol_table, &dba);
|
auto context = MakeContext(this->storage, symbol_table, &dba);
|
||||||
EXPECT_THROW(PullAll(*rem_op, &context), QueryRuntimeException);
|
ASSERT_THROW(CollectProduce(*produce, &context), QueryRuntimeException);
|
||||||
}
|
}
|
||||||
|
|
||||||
TYPED_TEST(QueryPlanTest, DeleteRemoveProperty) {
|
TYPED_TEST(QueryPlanTest, DeleteRemoveProperty) {
|
||||||
|
// MATCH (n) DELETE n REMOVE n.property RETURN n
|
||||||
auto storage_dba = this->db->Access();
|
auto storage_dba = this->db->Access();
|
||||||
memgraph::query::DbAccessor dba(storage_dba.get());
|
memgraph::query::DbAccessor dba(storage_dba.get());
|
||||||
// Add a single vertex.
|
// Add a single vertex.
|
||||||
@ -1847,15 +1877,20 @@ TYPED_TEST(QueryPlanTest, DeleteRemoveProperty) {
|
|||||||
dba.AdvanceCommand();
|
dba.AdvanceCommand();
|
||||||
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
|
||||||
SymbolTable symbol_table;
|
SymbolTable symbol_table;
|
||||||
// MATCH (n) DELETE n REMOVE n.property
|
|
||||||
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
auto n = MakeScanAll(this->storage, symbol_table, "n");
|
||||||
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
auto n_get = this->storage.template Create<Identifier>("n")->MapTo(n.sym_);
|
||||||
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
auto delete_op = std::make_shared<plan::Delete>(n.op_, std::vector<Expression *>{n_get}, false);
|
||||||
auto prop = PROPERTY_PAIR(dba, "property");
|
auto prop = PROPERTY_PAIR(dba, "property");
|
||||||
auto n_prop = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
auto n_prop = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
||||||
auto rem_op = std::make_shared<plan::RemoveProperty>(delete_op, prop.second, n_prop);
|
auto rem_op = std::make_shared<plan::RemoveProperty>(delete_op, prop.second, n_prop);
|
||||||
|
auto accumulate_op = std::make_shared<plan::Accumulate>(rem_op, rem_op->ModifiedSymbols(symbol_table), true);
|
||||||
|
|
||||||
|
auto prop_lookup = PROPERTY_LOOKUP(dba, IDENT("n")->MapTo(n.sym_), prop);
|
||||||
|
auto n_p =
|
||||||
|
this->storage.template Create<NamedExpression>("n", prop_lookup)->MapTo(symbol_table.CreateSymbol("bla", true));
|
||||||
|
auto produce = MakeProduce(accumulate_op, n_p);
|
||||||
auto context = MakeContext(this->storage, symbol_table, &dba);
|
auto context = MakeContext(this->storage, symbol_table, &dba);
|
||||||
EXPECT_THROW(PullAll(*rem_op, &context), QueryRuntimeException);
|
ASSERT_THROW(CollectProduce(*produce, &context), QueryRuntimeException);
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////
|
//////////////////////////////////////////////
|
||||||
|
Loading…
Reference in New Issue
Block a user