diff --git a/.github/workflows/diff.yaml b/.github/workflows/diff.yaml index 2b4ce5ded..c0794c596 100644 --- a/.github/workflows/diff.yaml +++ b/.github/workflows/diff.yaml @@ -248,22 +248,14 @@ jobs: tests/gql_behave/gql_behave_status.csv tests/gql_behave/gql_behave_status.html - - name: Run e2e replication tests + - name: Run e2e tests run: | # TODO(gitbuda): Setup mgclient and pymgclient properly. cd tests ./setup.sh source ve3/bin/activate cd e2e - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-path replication/workloads.yaml - - - name: Run e2e memory control tests - run: | - cd tests - ./setup.sh - source ve3/bin/activate - cd e2e - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-path memory/workloads.yaml + LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-root-directory . - name: Run stress test (plain) run: | diff --git a/.github/workflows/release_centos8.yaml b/.github/workflows/release_centos8.yaml index 17df9917c..e58ff5c88 100644 --- a/.github/workflows/release_centos8.yaml +++ b/.github/workflows/release_centos8.yaml @@ -293,22 +293,14 @@ jobs: tests/gql_behave/gql_behave_status.csv tests/gql_behave/gql_behave_status.html - - name: Run e2e replication tests + - name: Run e2e tests run: | # TODO(gitbuda): Setup mgclient and pymgclient properly. cd tests ./setup.sh source ve3/bin/activate cd e2e - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-path replication/workloads.yaml - - - name: Run e2e memory control tests - run: | - cd tests - ./setup.sh - source ve3/bin/activate - cd e2e - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-path memory/workloads.yaml + LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-root-directory . - name: Run stress test (plain) run: | diff --git a/.github/workflows/release_debian10.yaml b/.github/workflows/release_debian10.yaml index 2d5ba607a..30e9b5a85 100644 --- a/.github/workflows/release_debian10.yaml +++ b/.github/workflows/release_debian10.yaml @@ -291,22 +291,14 @@ jobs: tests/gql_behave/gql_behave_status.csv tests/gql_behave/gql_behave_status.html - - name: Run e2e replication tests + - name: Run e2e tests run: | # TODO(gitbuda): Setup mgclient and pymgclient properly. cd tests ./setup.sh source ve3/bin/activate cd e2e - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-path replication/workloads.yaml - - - name: Run e2e memory control tests - run: | - cd tests - ./setup.sh - source ve3/bin/activate - cd e2e - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-path memory/workloads.yaml + LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-root-directory . - name: Run stress test (plain) run: | diff --git a/.github/workflows/release_ubuntu2004.yaml b/.github/workflows/release_ubuntu2004.yaml index c54f3117b..d309d1946 100644 --- a/.github/workflows/release_ubuntu2004.yaml +++ b/.github/workflows/release_ubuntu2004.yaml @@ -291,22 +291,14 @@ jobs: tests/gql_behave/gql_behave_status.csv tests/gql_behave/gql_behave_status.html - - name: Run e2e replication tests + - name: Run e2e tests run: | # TODO(gitbuda): Setup mgclient and pymgclient properly. cd tests ./setup.sh source ve3/bin/activate cd e2e - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-path replication/workloads.yaml - - - name: Run e2e memory control tests - run: | - cd tests - ./setup.sh - source ve3/bin/activate - cd e2e - LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-path memory/workloads.yaml + LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib python runner.py --workloads-root-directory . - name: Run stress test (plain) run: | diff --git a/src/query/trigger.cpp b/src/query/trigger.cpp index be46d19e8..9f7edba84 100644 --- a/src/query/trigger.cpp +++ b/src/query/trigger.cpp @@ -168,10 +168,13 @@ TypedValue ToTypedValue(const std::vector &values, DbAccessor *dba) { } } - TypedValue result{std::map{}}; - auto &typed_values = result.ValueMap(); + TypedValue result{std::vector{}}; + auto &typed_values = result.ValueList(); for (auto &[label_id, vertices] : vertices_by_labels) { - typed_values.emplace(dba->LabelToName(label_id), TypedValue(std::move(vertices))); + typed_values.emplace_back(std::map{ + {std::string{"label"}, TypedValue(dba->LabelToName(label_id))}, + {std::string{"vertices"}, TypedValue(std::move(vertices))}, + }); } return result; diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index 9d6d7bd8e..6984f3530 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -1,3 +1,3 @@ add_subdirectory(replication) - add_subdirectory(memory) +add_subdirectory(triggers) diff --git a/tests/e2e/runner.py b/tests/e2e/runner.py index 5d88740d9..36a7a34de 100755 --- a/tests/e2e/runner.py +++ b/tests/e2e/runner.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser import atexit import logging import os +from pathlib import Path import subprocess import yaml @@ -17,18 +18,21 @@ log = logging.getLogger("memgraph.tests.e2e") def load_args(): parser = ArgumentParser() - parser.add_argument("--workloads-path", required=True) + parser.add_argument("--workloads-root-directory", required=True) parser.add_argument("--workload-name", default=None, required=False) return parser.parse_args() -def load_workloads(path): - with open(path, "r") as f: - return yaml.load(f, Loader=yaml.FullLoader)['workloads'] +def load_workloads(root_directory): + workloads = [] + for file in Path(root_directory).rglob('*.yaml'): + with open(file, "r") as f: + workloads.extend(yaml.load(f, Loader=yaml.FullLoader)['workloads']) + return workloads def run(args): - workloads = load_workloads(args.workloads_path) + workloads = load_workloads(args.workloads_root_directory) for workload in workloads: workload_name = workload['name'] if args.workload_name is not None and \ @@ -37,6 +41,7 @@ def run(args): log.info("%s STARTED.", workload_name) # Setup. mg_instances = {} + @atexit.register def cleanup(): for mg_instance in mg_instances.values(): diff --git a/tests/e2e/triggers/CMakeLists.txt b/tests/e2e/triggers/CMakeLists.txt new file mode 100644 index 000000000..d1587c56b --- /dev/null +++ b/tests/e2e/triggers/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library(memgraph__e2e__triggers_common STATIC common.hpp common.cpp) +target_link_libraries(memgraph__e2e__triggers_common PUBLIC gflags mgclient mg-utils) + +add_executable(memgraph__e2e__triggers__on_create on_create_triggers.cpp) +target_link_libraries(memgraph__e2e__triggers__on_create memgraph__e2e__triggers_common) + +add_executable(memgraph__e2e__triggers__on_update on_update_triggers.cpp) +target_link_libraries(memgraph__e2e__triggers__on_update memgraph__e2e__triggers_common) + +add_executable(memgraph__e2e__triggers__on_delete on_delete_triggers.cpp) +target_link_libraries(memgraph__e2e__triggers__on_delete memgraph__e2e__triggers_common) diff --git a/tests/e2e/triggers/common.cpp b/tests/e2e/triggers/common.cpp new file mode 100644 index 000000000..17f916217 --- /dev/null +++ b/tests/e2e/triggers/common.cpp @@ -0,0 +1,94 @@ +#include "common.hpp" + +#include +#include + +#include +#include +#include "utils/logging.hpp" +#include "utils/timer.hpp" + +DEFINE_uint64(bolt_port, 7687, "Bolt port"); + +std::unique_ptr Connect() { + auto client = + mg::Client::Connect({.host = "127.0.0.1", .port = static_cast(FLAGS_bolt_port), .use_ssl = false}); + MG_ASSERT(client, "Failed to connect!"); + return client; +} + +void CreateVertex(mg::Client &client, int vertex_id) { + mg::Map parameters{ + {"id", mg::Value{vertex_id}}, + }; + client.Execute(fmt::format("CREATE (n: {} {{ id: $id }})", kVertexLabel), mg::ConstMap{parameters.ptr()}); + client.DiscardAll(); +} + +void CreateEdge(mg::Client &client, int from_vertex, int to_vertex, int edge_id) { + mg::Map parameters{ + {"from", mg::Value{from_vertex}}, + {"to", mg::Value{to_vertex}}, + {"id", mg::Value{edge_id}}, + }; + client.Execute(fmt::format("MATCH (from: {} {{ id: $from }}), (to: {} {{id: $to }}) " + "CREATE (from)-[r: {} {{id: $id}}]->(to)", + kVertexLabel, kVertexLabel, kEdgeLabel), + mg::ConstMap{parameters.ptr()}); + client.DiscardAll(); +} + +int GetNumberOfAllVertices(mg::Client &client) { + client.Execute("MATCH (n) RETURN COUNT(*)"); + const auto value = client.FetchOne(); + MG_ASSERT(value, "Unexpected error"); + MG_ASSERT(value->size() == 1, "Unexpected number of columns!"); + client.FetchAll(); + MG_ASSERT(value->at(0).type() == mg::Value::Type::Int, "Unexpected type!"); + return value->at(0).ValueInt(); +} + +void WaitForNumberOfAllVertices(mg::Client &client, int number_of_vertices) { + utils::Timer timer{}; + while ((timer.Elapsed().count() <= 0.5) && GetNumberOfAllVertices(client) != number_of_vertices) { + } + CheckNumberOfAllVertices(client, number_of_vertices); +} + +void CheckNumberOfAllVertices(mg::Client &client, int expected_number_of_vertices) { + const auto number_of_vertices = GetNumberOfAllVertices(client); + MG_ASSERT(number_of_vertices == expected_number_of_vertices, "There are {} vertices, expected {}!", + number_of_vertices, expected_number_of_vertices); +} + +std::optional GetVertex(mg::Client &client, std::string_view label, int vertex_id) { + mg::Map parameters{ + {"id", mg::Value{vertex_id}}, + }; + + client.Execute(fmt::format("MATCH (n: {} {{id: $id}}) RETURN n", label), mg::ConstMap{parameters.ptr()}); + const auto result = client.FetchAll(); + MG_ASSERT(result, "Vertex with label {} and id {} cannot be found!", label, vertex_id); + const auto &rows = *result; + MG_ASSERT(rows.size() <= 1, "Unexpected number of vertices with label {} and id {}, found {} vertices", label, + vertex_id, rows.size()); + if (rows.empty()) { + return std::nullopt; + } + + return rows[0][0]; +} + +bool VertexExists(mg::Client &client, std::string_view label, int vertex_id) { + return GetVertex(client, label, vertex_id).has_value(); +} + +void CheckVertexMissing(mg::Client &client, std::string_view label, int vertex_id) { + MG_ASSERT(!VertexExists(client, label, vertex_id), "Not expected vertex exist with label {} and id {}!", label, + vertex_id); +} + +void CheckVertexExists(mg::Client &client, std::string_view label, int vertex_id) { + MG_ASSERT(VertexExists(client, label, vertex_id), "Expected vertex doesn't exist with label {} and id {}!", label, + vertex_id); +} diff --git a/tests/e2e/triggers/common.hpp b/tests/e2e/triggers/common.hpp new file mode 100644 index 000000000..903dd8125 --- /dev/null +++ b/tests/e2e/triggers/common.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +#include + +constexpr std::string_view kVertexLabel{"VERTEX"}; +constexpr std::string_view kEdgeLabel{"EDGE"}; + +std::unique_ptr Connect(); +void CreateVertex(mg::Client &client, int vertex_id); +void CreateEdge(mg::Client &client, int from_vertex, int to_vertex, int edge_id); + +int GetNumberOfAllVertices(mg::Client &client); +void WaitForNumberOfAllVertices(mg::Client &client, int number_of_vertices); +void CheckNumberOfAllVertices(mg::Client &client, int expected_number_of_vertices); +std::optional GetVertex(mg::Client &client, std::string_view label, int vertex_id); +bool VertexExists(mg::Client &client, std::string_view label, int vertex_id); +void CheckVertexMissing(mg::Client &client, std::string_view label, int vertex_id); +void CheckVertexExists(mg::Client &client, std::string_view label, int vertex_id); \ No newline at end of file diff --git a/tests/e2e/triggers/on_create_triggers.cpp b/tests/e2e/triggers/on_create_triggers.cpp new file mode 100644 index 000000000..d2699ddcf --- /dev/null +++ b/tests/e2e/triggers/on_create_triggers.cpp @@ -0,0 +1,107 @@ +#include +#include + +#include +#include +#include "common.hpp" +#include "utils/logging.hpp" + +constexpr std::string_view kTriggerCreatedVertexLabel{"CREATED_VERTEX"}; +constexpr std::string_view kTriggerCreatedEdgeLabel{"CREATED_EDGE"}; +constexpr std::string_view kTriggerCreatedObjectLabel{"CREATED_OBJECT"}; + +void CreateOnCreateTriggers(mg::Client &client, bool is_before) { + const std::string_view before_or_after = is_before ? "BEFORE" : "AFTER"; + client.Execute( + fmt::format("CREATE TRIGGER CreatedVerticesTrigger ON () CREATE " + "{} COMMIT " + "EXECUTE " + "UNWIND createdVertices as createdVertex " + "CREATE (n: {} {{ id: createdVertex.id }})", + before_or_after, kTriggerCreatedVertexLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER CreatedEdgesTrigger ON --> CREATE " + "{} COMMIT " + "EXECUTE " + "UNWIND createdEdges as createdEdge " + "CREATE (n: {} {{ id: createdEdge.id }})", + before_or_after, kTriggerCreatedEdgeLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER CreatedObjectsTrigger ON CREATE " + "{} COMMIT " + "EXECUTE " + "UNWIND createdObjects as createdObjectEvent " + "WITH CASE createdObjectEvent.event_type WHEN \"created_vertex\" THEN createdObjectEvent.vertex.id " + "ELSE createdObjectEvent.edge.id END as id " + "CREATE (n: {} {{ id: id }})", + before_or_after, kTriggerCreatedObjectLabel)); + client.DiscardAll(); +} + +void DropOnCreateTriggers(mg::Client &client) { + client.Execute("DROP TRIGGER CreatedVerticesTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER CreatedEdgesTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER CreatedObjectsTrigger"); + client.DiscardAll(); +} + +int main(int argc, char **argv) { + gflags::SetUsageMessage("Memgraph E2E ON CREATE Triggers"); + gflags::ParseCommandLineFlags(&argc, &argv, true); + logging::RedirectToStderr(); + + mg::Client::Init(); + + auto client = Connect(); + + const auto run_create_trigger_tests = [&](bool is_before) { + const std::array vertex_ids{1, 2}; + const int edge_id = 3; + { + CreateOnCreateTriggers(*client, is_before); + client->BeginTransaction(); + for (const auto vertex_id : vertex_ids) { + CreateVertex(*client, vertex_id); + CheckVertexExists(*client, kVertexLabel, vertex_id); + CheckVertexMissing(*client, kTriggerCreatedVertexLabel, vertex_id); + CheckVertexMissing(*client, kTriggerCreatedObjectLabel, vertex_id); + } + CreateEdge(*client, vertex_ids[0], vertex_ids[1], edge_id); + CheckVertexMissing(*client, kTriggerCreatedEdgeLabel, edge_id); + CheckVertexMissing(*client, kTriggerCreatedObjectLabel, edge_id); + client->CommitTransaction(); + + // :VERTEX x 2 + // :CREATED_VERTEX x 2 + // :CREATED_EDGE x 1 + // :CREATED_OBJECT x 3 + constexpr auto kNumberOfExpectedVertices = 8; + + if (is_before) { + CheckNumberOfAllVertices(*client, kNumberOfExpectedVertices); + } else { + WaitForNumberOfAllVertices(*client, kNumberOfExpectedVertices); + } + + for (const auto vertex_id : vertex_ids) { + CheckVertexExists(*client, kTriggerCreatedVertexLabel, vertex_id); + CheckVertexExists(*client, kTriggerCreatedObjectLabel, vertex_id); + } + CheckVertexExists(*client, kTriggerCreatedEdgeLabel, edge_id); + CheckVertexExists(*client, kTriggerCreatedObjectLabel, edge_id); + DropOnCreateTriggers(*client); + client->Execute("MATCH (n) DETACH DELETE n;"); + client->DiscardAll(); + } + }; + constexpr bool kBeforeCommit = true; + constexpr bool kAfterCommit = false; + run_create_trigger_tests(kBeforeCommit); + run_create_trigger_tests(kAfterCommit); + + return 0; +} diff --git a/tests/e2e/triggers/on_delete_triggers.cpp b/tests/e2e/triggers/on_delete_triggers.cpp new file mode 100644 index 000000000..1c0886863 --- /dev/null +++ b/tests/e2e/triggers/on_delete_triggers.cpp @@ -0,0 +1,133 @@ +#include +#include + +#include +#include +#include "common.hpp" +#include "utils/logging.hpp" + +constexpr std::string_view kTriggerDeletedVertexLabel{"DELETED_VERTEX"}; +constexpr std::string_view kTriggerDeletedEdgeLabel{"DELETED_EDGE"}; +constexpr std::string_view kTriggerDeletedObjectLabel{"DELETED_OBJECT"}; + +void DetachDeleteVertex(mg::Client &client, int vertex_id) { + mg::Map parameters{{"id", mg::Value{vertex_id}}}; + client.Execute(fmt::format("MATCH (n: {} {{id: $id}}) DETACH DELETE n", kVertexLabel), + mg::ConstMap{parameters.ptr()}); + client.DiscardAll(); +} + +void DeleteEdge(mg::Client &client, int edge_id) { + mg::Map parameters{{"id", mg::Value{edge_id}}}; + client.Execute(fmt::format("MATCH ()-[r: {} {{id: $id}}]->() DELETE r", kEdgeLabel), mg::ConstMap{parameters.ptr()}); + client.DiscardAll(); +} + +void CreateOnDeleteTriggers(mg::Client &client, bool is_before) { + const std::string_view before_or_after = is_before ? "BEFORE" : "AFTER"; + client.Execute( + fmt::format("CREATE TRIGGER DeletedVerticesTrigger ON () DELETE " + "{} COMMIT " + "EXECUTE " + "UNWIND deletedVertices as deletedVertex " + "CREATE (n: {} {{ id: deletedVertex.id }})", + before_or_after, kTriggerDeletedVertexLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER DeletedEdgesTrigger ON --> DELETE " + "{} COMMIT " + "EXECUTE " + "UNWIND deletedEdges as deletedEdge " + "CREATE (n: {} {{ id: deletedEdge.id }})", + before_or_after, kTriggerDeletedEdgeLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER DeletedObjectsTrigger ON DELETE " + "{} COMMIT " + "EXECUTE " + "UNWIND deletedObjects as deletedObjectEvent " + "WITH CASE deletedObjectEvent.event_type WHEN \"deleted_vertex\" THEN deletedObjectEvent.vertex.id " + "ELSE deletedObjectEvent.edge.id END as id " + "CREATE (n: {} {{ id: id }})", + before_or_after, kTriggerDeletedObjectLabel)); + client.DiscardAll(); +} + +void DropOnDeleteTriggers(mg::Client &client) { + client.Execute("DROP TRIGGER DeletedVerticesTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER DeletedEdgesTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER DeletedObjectsTrigger"); + client.DiscardAll(); +} + +struct EdgeInfo { + int from_vertex; + int to_vertex; + int edge_id; +}; + +int main(int argc, char **argv) { + gflags::SetUsageMessage("Memgraph E2E ON DELETE Triggers"); + gflags::ParseCommandLineFlags(&argc, &argv, true); + logging::RedirectToStderr(); + + mg::Client::Init(); + + auto client = Connect(); + + const auto run_delete_trigger_tests = [&](bool is_before) { + const std::array vertex_ids{1, 2, 3, 4}; + const std::array edges{EdgeInfo{vertex_ids[0], vertex_ids[1], 5}, EdgeInfo{vertex_ids[2], vertex_ids[3], 6}}; + { + CreateOnDeleteTriggers(*client, is_before); + + client->BeginTransaction(); + for (const auto vertex_id : vertex_ids) { + CreateVertex(*client, vertex_id); + } + for (const auto &edge : edges) { + CreateEdge(*client, edge.from_vertex, edge.to_vertex, edge.edge_id); + } + client->CommitTransaction(); + CheckNumberOfAllVertices(*client, vertex_ids.size()); + + client->BeginTransaction(); + DetachDeleteVertex(*client, vertex_ids[0]); + DeleteEdge(*client, edges[1].edge_id); + client->CommitTransaction(); + + // :VERTEX x 4 + // deleted :VERTEX x -1 + // :DELETED_VERTEX x 1 + // :DELETED_EDGE x 2 + // :DELETED_OBJECT x 3 + constexpr auto kNumberOfExpectedVertices = 9; + + if (is_before) { + CheckNumberOfAllVertices(*client, kNumberOfExpectedVertices); + } else { + WaitForNumberOfAllVertices(*client, kNumberOfExpectedVertices); + } + + CheckVertexExists(*client, kTriggerDeletedVertexLabel, vertex_ids[0]); + CheckVertexExists(*client, kTriggerDeletedObjectLabel, vertex_ids[0]); + + for (const auto &edge : edges) { + CheckVertexExists(*client, kTriggerDeletedEdgeLabel, edge.edge_id); + CheckVertexExists(*client, kTriggerDeletedObjectLabel, edge.edge_id); + } + + DropOnDeleteTriggers(*client); + client->Execute("MATCH (n) DETACH DELETE n;"); + client->DiscardAll(); + } + }; + constexpr bool kBeforeCommit = true; + constexpr bool kAfterCommit = false; + run_delete_trigger_tests(kBeforeCommit); + run_delete_trigger_tests(kAfterCommit); + + return 0; +} diff --git a/tests/e2e/triggers/on_update_triggers.cpp b/tests/e2e/triggers/on_update_triggers.cpp new file mode 100644 index 000000000..75da49ac8 --- /dev/null +++ b/tests/e2e/triggers/on_update_triggers.cpp @@ -0,0 +1,286 @@ +#include +#include + +#include +#include +#include "common.hpp" +#include "utils/logging.hpp" + +constexpr std::string_view kTriggerUpdatedVertexLabel{"UPDATED_VERTEX"}; +constexpr std::string_view kTriggerUpdatedEdgeLabel{"UPDATED_EDGE"}; +constexpr std::string_view kTriggerUpdatedObjectLabel{"UPDATED_OBJECT"}; +constexpr std::string_view kTriggerSetVertexPropertyLabel{"SET_VERTEX_PROPERTY"}; +constexpr std::string_view kTriggerRemovedVertexPropertyLabel{"REMOVED_VERTEX_PROPERTY"}; +constexpr std::string_view kTriggerSetVertexLabelLabel{"SET_VERTEX_LABEL"}; +constexpr std::string_view kTriggerRemovedVertexLabelLabel{"REMOVED_VERTEX_LABEL"}; +constexpr std::string_view kTriggerSetEdgePropertyLabel{"SET_EDGE_PROPERTY"}; +constexpr std::string_view kTriggerRemovedEdgePropertyLabel{"REMOVED_EDGE_PROPERTY"}; + +void SetVertexProperty(mg::Client &client, int vertex_id, std::string_view property_name, mg::Value value) { + mg::Map parameters{ + {"id", mg::Value{vertex_id}}, + {"value", std::move(value)}, + }; + client.Execute(fmt::format("MATCH (n: {} {{id: $id}}) " + "SET n.{} = $value", + kVertexLabel, property_name), + mg::ConstMap{parameters.ptr()}); + client.DiscardAll(); +} + +void SetEdgeProperty(mg::Client &client, int edge_id, std::string_view property_name, mg::Value value) { + mg::Map parameters{ + {"id", mg::Value{edge_id}}, + {"value", std::move(value)}, + }; + client.Execute(fmt::format("MATCH ()-[r: {} {{id: $id}}]->() " + "SET r.{} = $value", + kEdgeLabel, property_name), + mg::ConstMap{parameters.ptr()}); + client.DiscardAll(); +} + +void DoVertexLabelOperation(mg::Client &client, int vertex_id, std::string_view label, std::string_view operation) { + mg::Map parameters{{"id", mg::Value{vertex_id}}}; + client.Execute(fmt::format("MATCH (n: {} {{id: $id}}) " + "{} n:{}", + kVertexLabel, operation, label), + mg::ConstMap{parameters.ptr()}); + client.DiscardAll(); +} + +void AddVertexLabel(mg::Client &client, int vertex_id, std::string_view label) { + DoVertexLabelOperation(client, vertex_id, label, "SET"); +} + +void RemoveVertexLabel(mg::Client &client, int vertex_id, std::string_view label) { + DoVertexLabelOperation(client, vertex_id, label, "REMOVE"); +} + +void CheckVertexProperty(mg::Client &client, std::string_view label, int vertex_id, std::string_view property_name, + const mg::Value &value) { + const auto vertex = GetVertex(client, label, vertex_id); + MG_ASSERT(vertex, "Cannot check property of not existing vertex with label {} and id {}", label, vertex_id); + + const auto properties = vertex->ValueNode().properties(); + const auto prop_it = properties.find(property_name); + MG_ASSERT(prop_it != properties.end(), "Vertex with label {} and id {} doesn't have expected property {}!", label, + vertex_id, property_name); + MG_ASSERT((*prop_it).second == value, "Property {} of vertex with label {} and id {} doesn't have expected value!", + property_name, label, vertex_id); +} + +void CreateOnUpdateTriggers(mg::Client &client, bool is_before) { + const std::string_view before_or_after = is_before ? "BEFORE" : "AFTER"; + client.Execute( + fmt::format("CREATE TRIGGER UpdatedVerticesTrigger ON () UPDATE " + "{} COMMIT " + "EXECUTE " + "UNWIND updatedVertices as updateVertexEvent " + "CREATE (n: {} {{ id: updateVertexEvent.vertex.id , event_type: updateVertexEvent.event_type }})", + before_or_after, kTriggerUpdatedVertexLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER UpdatedEdgesTrigger ON --> UPDATE " + "{} COMMIT " + "EXECUTE " + "UNWIND updatedEdges as updatedEdgeEvent " + "CREATE (n: {} {{ id: updatedEdgeEvent.edge.id, event_type: updatedEdgeEvent.event_type }})", + before_or_after, kTriggerUpdatedEdgeLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER UpdatedObjectsTrigger ON UPDATE " + "{} COMMIT " + "EXECUTE " + "UNWIND updatedObjects as updatedObject " + "WITH CASE updatedObject.event_type " + "WHEN \"set_edge_property\" THEN updatedObject.edge.id " + "WHEN \"removed_edge_property\" THEN updatedObject.edge.id " + "ELSE updatedObject.vertex.id END as id, updatedObject " + "CREATE (n: {} {{ id: id, event_type: updatedObject.event_type }})", + before_or_after, kTriggerUpdatedObjectLabel)); + client.DiscardAll(); + + client.Execute( + fmt::format("CREATE TRIGGER SetVertexPropertiesTrigger ON () UPDATE " + "{} COMMIT " + "EXECUTE " + "UNWIND assignedVertexProperties as assignedVertexProperty " + "CREATE (n: {} {{ id: assignedVertexProperty.vertex.id }})", + before_or_after, kTriggerSetVertexPropertyLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER RemovedVertexPropertiesTrigger ON () UPDATE " + "{} COMMIT " + "EXECUTE " + "UNWIND removedVertexProperties as removedVertexProperty " + "CREATE (n: {} {{ id: removedVertexProperty.vertex.id }})", + before_or_after, kTriggerRemovedVertexPropertyLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER SetVertexLabelsTrigger ON () UPDATE " + "{} COMMIT " + "EXECUTE " + "UNWIND assignedVertexLabels as assignedVertexLabel " + "UNWIND assignedVertexLabel.vertices as vertex " + "CREATE (n: {} {{ id: vertex.id }})", + before_or_after, kTriggerSetVertexLabelLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER RemovedVertexLabelTrigger ON () UPDATE " + "{} COMMIT " + "EXECUTE " + "UNWIND removedVertexLabels as removedVertexLabel " + "UNWIND removedVertexLabel.vertices as vertex " + "CREATE (n: {} {{ id: vertex.id }})", + before_or_after, kTriggerRemovedVertexLabelLabel)); + client.DiscardAll(); + + client.Execute( + fmt::format("CREATE TRIGGER SetEdgePropertiesTrigger ON --> UPDATE " + "{} COMMIT " + "EXECUTE " + "UNWIND assignedEdgeProperties as assignedEdgeProperty " + "CREATE (n: {} {{ id: assignedEdgeProperty.edge.id }})", + before_or_after, kTriggerSetEdgePropertyLabel)); + client.DiscardAll(); + client.Execute( + fmt::format("CREATE TRIGGER RemovedEdgePropertiesTrigger ON --> UPDATE " + "{} COMMIT " + "EXECUTE " + "UNWIND removedEdgeProperties as removedEdgeProperty " + "CREATE (n: {} {{ id: removedEdgeProperty.edge.id }})", + before_or_after, kTriggerRemovedEdgePropertyLabel)); + client.DiscardAll(); +} + +void DropOnUpdateTriggers(mg::Client &client) { + client.Execute("DROP TRIGGER UpdatedVerticesTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER UpdatedEdgesTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER UpdatedObjectsTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER SetVertexPropertiesTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER RemoveVertexPropertiesTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER SetVertexLabelsTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER RemovedVertexLabelTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER SetEdgePropertiesTrigger"); + client.DiscardAll(); + client.Execute("DROP TRIGGER RemovedEdgePropertiesTrigger"); + client.DiscardAll(); +} + +struct EdgeInfo { + int from_vertex; + int to_vertex; + int edge_id; +}; + +int main(int argc, char **argv) { + constexpr std::string_view kExtraLabel = "EXTRA_LABEL"; + constexpr std::string_view kUpdatedProperty = "updateProperty"; + gflags::SetUsageMessage("Memgraph E2E ON UPDATE Triggers"); + gflags::ParseCommandLineFlags(&argc, &argv, true); + logging::RedirectToStderr(); + + mg::Client::Init(); + + auto client = Connect(); + + const auto run_update_trigger_tests = [&](bool is_before) { + const std::array vertex_ids{1, 2, 3, 4}; + const std::array edges{EdgeInfo{vertex_ids[0], vertex_ids[1], 5}, EdgeInfo{vertex_ids[2], vertex_ids[3], 6}}; + { + CreateOnUpdateTriggers(*client, is_before); + + client->BeginTransaction(); + for (const auto vertex_id : vertex_ids) { + CreateVertex(*client, vertex_id); + SetVertexProperty(*client, vertex_id, kUpdatedProperty, mg::Value(vertex_id)); + AddVertexLabel(*client, vertex_id, kExtraLabel); + } + for (const auto &edge : edges) { + CreateEdge(*client, edge.from_vertex, edge.to_vertex, edge.edge_id); + SetEdgeProperty(*client, edge.edge_id, kUpdatedProperty, mg::Value(edge.edge_id)); + } + client->CommitTransaction(); + CheckNumberOfAllVertices(*client, vertex_ids.size()); + + client->BeginTransaction(); + SetVertexProperty(*client, vertex_ids[0], kUpdatedProperty, mg::Value(-1)); + SetVertexProperty(*client, vertex_ids[1], kUpdatedProperty, mg::Value()); + AddVertexLabel(*client, vertex_ids[2], "NEW_LABEL"); + RemoveVertexLabel(*client, vertex_ids[3], kExtraLabel); + SetEdgeProperty(*client, edges[0].edge_id, kUpdatedProperty, mg::Value(-1)); + SetEdgeProperty(*client, edges[1].edge_id, kUpdatedProperty, mg::Value()); + CheckNumberOfAllVertices(*client, vertex_ids.size()); + client->CommitTransaction(); + + // :VERTEX x 4 + // :UPDATED_VERTEX x 4 + // :UPDATED_EDGE x 2 + // :UPDATED_OBJECT x 6 + // :SET_VERTEX_PROPERTY x 1 + // :REMOVED_VERTEX_PROPERTY x 1 + // :SET_VERTEX_LABEL x 1 + // :REMOVED_VERTEX_LABEL x 1 + // :SET_EDGE_PROPERTY x 1 + // :REMOVED_EDGE_PROPERTY x 1 + constexpr auto kNumberOfExpectedVertices = 22; + + if (is_before) { + CheckNumberOfAllVertices(*client, kNumberOfExpectedVertices); + } else { + WaitForNumberOfAllVertices(*client, kNumberOfExpectedVertices); + } + + CheckVertexProperty(*client, kTriggerUpdatedVertexLabel, vertex_ids[0], "event_type", + mg::Value{"set_vertex_property"}); + CheckVertexProperty(*client, kTriggerUpdatedVertexLabel, vertex_ids[1], "event_type", + mg::Value{"removed_vertex_property"}); + CheckVertexProperty(*client, kTriggerUpdatedVertexLabel, vertex_ids[2], "event_type", + mg::Value{"set_vertex_label"}); + CheckVertexProperty(*client, kTriggerUpdatedVertexLabel, vertex_ids[3], "event_type", + mg::Value{"removed_vertex_label"}); + CheckVertexProperty(*client, kTriggerUpdatedEdgeLabel, edges[0].edge_id, "event_type", + mg::Value{"set_edge_property"}); + CheckVertexProperty(*client, kTriggerUpdatedEdgeLabel, edges[1].edge_id, "event_type", + mg::Value{"removed_edge_property"}); + + CheckVertexProperty(*client, kTriggerUpdatedObjectLabel, vertex_ids[0], "event_type", + mg::Value{"set_vertex_property"}); + CheckVertexProperty(*client, kTriggerUpdatedObjectLabel, vertex_ids[1], "event_type", + mg::Value{"removed_vertex_property"}); + CheckVertexProperty(*client, kTriggerUpdatedObjectLabel, vertex_ids[2], "event_type", + mg::Value{"set_vertex_label"}); + CheckVertexProperty(*client, kTriggerUpdatedObjectLabel, vertex_ids[3], "event_type", + mg::Value{"removed_vertex_label"}); + CheckVertexProperty(*client, kTriggerUpdatedObjectLabel, edges[0].edge_id, "event_type", + mg::Value{"set_edge_property"}); + CheckVertexProperty(*client, kTriggerUpdatedObjectLabel, edges[1].edge_id, "event_type", + mg::Value{"removed_edge_property"}); + + CheckVertexExists(*client, kTriggerSetVertexPropertyLabel, vertex_ids[0]); + CheckVertexExists(*client, kTriggerRemovedVertexPropertyLabel, vertex_ids[1]); + CheckVertexExists(*client, kTriggerSetVertexLabelLabel, vertex_ids[2]); + CheckVertexExists(*client, kTriggerRemovedVertexLabelLabel, vertex_ids[3]); + CheckVertexExists(*client, kTriggerSetEdgePropertyLabel, edges[0].edge_id); + CheckVertexExists(*client, kTriggerRemovedEdgePropertyLabel, edges[1].edge_id); + + DropOnUpdateTriggers(*client); + client->Execute("MATCH (n) DETACH DELETE n;"); + client->DiscardAll(); + } + }; + constexpr bool kBeforeCommit = true; + constexpr bool kAfterCommit = false; + run_update_trigger_tests(kBeforeCommit); + run_update_trigger_tests(kAfterCommit); + + return 0; +} diff --git a/tests/e2e/triggers/workloads.yaml b/tests/e2e/triggers/workloads.yaml new file mode 100644 index 000000000..55d82d485 --- /dev/null +++ b/tests/e2e/triggers/workloads.yaml @@ -0,0 +1,24 @@ +bolt_port: &bolt_port "7687" +template_cluster: &template_cluster + cluster: + main: + args: ["--bolt-port", *bolt_port, "--log-level=TRACE"] + log_file: "triggers-e2e.log" + setup_queries: [] + validation_queries: [] + +workloads: + - name: "ON CREATE Triggers" + binary: "tests/e2e/triggers/memgraph__e2e__triggers__on_create" + args: ["--bolt-port", *bolt_port] + <<: *template_cluster + - name: "ON UPDATE Triggers" + binary: "tests/e2e/triggers/memgraph__e2e__triggers__on_update" + args: ["--bolt-port", *bolt_port] + <<: *template_cluster + - name: "ON DELETE Triggers" + binary: "tests/e2e/triggers/memgraph__e2e__triggers__on_delete" + args: ["--bolt-port", *bolt_port] + <<: *template_cluster + + diff --git a/tests/unit/query_trigger.cpp b/tests/unit/query_trigger.cpp index a5bb3ad91..e14822208 100644 --- a/tests/unit/query_trigger.cpp +++ b/tests/unit/query_trigger.cpp @@ -34,15 +34,23 @@ void CheckTypedValueSize(const query::TriggerContext &trigger_context, const que ASSERT_EQ(typed_values.ValueList().size(), expected_size); }; -void CheckLabelMap(const query::TriggerContext &trigger_context, const query::TriggerIdentifierTag tag, - const size_t expected, query::DbAccessor &dba) { +void CheckLabelList(const query::TriggerContext &trigger_context, const query::TriggerIdentifierTag tag, + const size_t expected, query::DbAccessor &dba) { auto typed_values = trigger_context.GetTypedValue(tag, &dba); - ASSERT_TRUE(typed_values.IsMap()); - auto &typed_values_map = typed_values.ValueMap(); + ASSERT_TRUE(typed_values.IsList()); + const auto &label_maps = typed_values.ValueList(); size_t value_count = 0; - for (const auto &[label, values] : typed_values_map) { - ASSERT_TRUE(values.IsList()); - value_count += values.ValueList().size(); + for (const auto &label_map : label_maps) { + ASSERT_TRUE(label_map.IsMap()); + const auto &typed_values_map = label_map.ValueMap(); + ASSERT_EQ(typed_values_map.size(), 2); + const auto label_it = typed_values_map.find("label"); + ASSERT_NE(label_it, typed_values_map.end()); + ASSERT_TRUE(label_it->second.IsString()); + const auto vertices_it = typed_values_map.find("vertices"); + ASSERT_NE(vertices_it, typed_values_map.end()); + ASSERT_TRUE(vertices_it->second.IsList()); + value_count += vertices_it->second.ValueList().size(); } ASSERT_EQ(value_count, expected); }; @@ -181,8 +189,8 @@ TEST_F(TriggerContextTest, ValidObjectsTest) { CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_PROPERTIES, vertex_count, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::REMOVED_EDGE_PROPERTIES, edge_count, dba); - CheckLabelMap(trigger_context, query::TriggerIdentifierTag::SET_VERTEX_LABELS, vertex_count, dba); - CheckLabelMap(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_LABELS, vertex_count, dba); + CheckLabelList(trigger_context, query::TriggerIdentifierTag::SET_VERTEX_LABELS, vertex_count, dba); + CheckLabelList(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_LABELS, vertex_count, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::UPDATED_VERTICES, 4 * vertex_count, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::UPDATED_EDGES, 2 * edge_count, dba); @@ -223,8 +231,8 @@ TEST_F(TriggerContextTest, ValidObjectsTest) { CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_PROPERTIES, vertex_count, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::REMOVED_EDGE_PROPERTIES, edge_count, dba); - CheckLabelMap(trigger_context, query::TriggerIdentifierTag::SET_VERTEX_LABELS, vertex_count, dba); - CheckLabelMap(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_LABELS, vertex_count, dba); + CheckLabelList(trigger_context, query::TriggerIdentifierTag::SET_VERTEX_LABELS, vertex_count, dba); + CheckLabelList(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_LABELS, vertex_count, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::UPDATED_VERTICES, 4 * vertex_count, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::UPDATED_EDGES, 2 * edge_count, dba); @@ -282,8 +290,8 @@ TEST_F(TriggerContextTest, ReturnCreateOnlyEvent) { CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_PROPERTIES, 0, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::REMOVED_EDGE_PROPERTIES, 0, dba); - CheckLabelMap(trigger_context, query::TriggerIdentifierTag::SET_VERTEX_LABELS, 0, dba); - CheckLabelMap(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_LABELS, 0, dba); + CheckLabelList(trigger_context, query::TriggerIdentifierTag::SET_VERTEX_LABELS, 0, dba); + CheckLabelList(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_LABELS, 0, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::UPDATED_VERTICES, 0, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::UPDATED_EDGES, 0, dba);