// Copyright 2022 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source // License, and you may not use this file except in compliance with the Business Source License. // // As of the Change Date specified in that file, in accordance with // the Business Source License, use of this software will be governed // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. #include #include #include #include #include #include "common.hpp" #include "utils/logging.hpp" inline constexpr std::string_view kTriggerDeletedVertexLabel{"DELETED_VERTEX"}; inline constexpr std::string_view kTriggerDeletedEdgeLabel{"DELETED_EDGE"}; inline constexpr std::string_view kTriggerDeletedObjectLabel{"DELETED_OBJECT"}; enum class AllowedTriggerType : uint8_t { VERTEX, EDGE, 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::unordered_set &allowed_trigger_types) { const std::string_view before_or_after = is_before ? "BEFORE" : "AFTER"; const auto create_on_vertex_delete_trigger = [&, before_or_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(); }; const auto create_on_edge_delete_trigger = [&, before_or_after] { 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(); }; const auto create_on_object_delete_trigger = [&, before_or_after] { 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(); }; for (const auto allowed_trigger_type : allowed_trigger_types) { switch (allowed_trigger_type) { case AllowedTriggerType::VERTEX: create_on_vertex_delete_trigger(); break; case AllowedTriggerType::EDGE: create_on_edge_delete_trigger(); break; case AllowedTriggerType::OBJECT: create_on_object_delete_trigger(); break; } } } void DropOnDeleteTriggers(mg::Client &client, const std::unordered_set &allowed_trigger_types) { for (const auto allowed_trigger_type : allowed_trigger_types) { switch (allowed_trigger_type) { case AllowedTriggerType::VERTEX: { client.Execute("DROP TRIGGER DeletedVerticesTrigger"); client.DiscardAll(); break; } case AllowedTriggerType::EDGE: { client.Execute("DROP TRIGGER DeletedEdgesTrigger"); client.DiscardAll(); break; } case AllowedTriggerType::OBJECT: { client.Execute("DROP TRIGGER DeletedObjectsTrigger"); client.DiscardAll(); break; } } } } struct EdgeInfo { int from_vertex; int to_vertex; int edge_id; }; void ValidateVertexExistance(mg::Client &client, const bool should_exist, const std::string_view label, const int id) { should_exist ? CheckVertexExists(client, label, id) : CheckVertexMissing(client, label, id); }; int main(int argc, char **argv) { gflags::SetUsageMessage("Memgraph E2E ON DELETE Triggers"); gflags::ParseCommandLineFlags(&argc, &argv, true); memgraph::logging::RedirectToStderr(); mg::Client::Init(); auto client = Connect(); const auto run_delete_trigger_tests = [&](const bool is_before, const std::unordered_set &allowed_trigger_types) { static constexpr std::array vertex_ids{1, 2, 3, 4}; static constexpr std::array edges{EdgeInfo{vertex_ids[0], vertex_ids[1], 5}, EdgeInfo{vertex_ids[2], vertex_ids[3], 6}}; { CreateOnDeleteTriggers(*client, is_before, allowed_trigger_types); 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 auto number_of_expected_vertices = 3; for (const auto allowed_trigger_type : allowed_trigger_types) { switch (allowed_trigger_type) { case AllowedTriggerType::VERTEX: number_of_expected_vertices += 1; break; case AllowedTriggerType::EDGE: number_of_expected_vertices += 2; break; case AllowedTriggerType::OBJECT: number_of_expected_vertices += 3; break; } } if (is_before) { CheckNumberOfAllVertices(*client, number_of_expected_vertices); } else { WaitForNumberOfAllVertices(*client, number_of_expected_vertices); } ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::VERTEX), kTriggerDeletedVertexLabel, vertex_ids[0]); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::OBJECT), kTriggerDeletedObjectLabel, vertex_ids[0]); for (const auto &edge : edges) { ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::EDGE), kTriggerDeletedEdgeLabel, edge.edge_id); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::OBJECT), kTriggerDeletedObjectLabel, edge.edge_id); } DropOnDeleteTriggers(*client, allowed_trigger_types); client->Execute("MATCH (n) DETACH DELETE n;"); client->DiscardAll(); } }; const auto run_delete_trigger_write_procedure_tests = [&](const std::unordered_set &allowed_trigger_types) { ExecuteCreateVertex(*client, 2); ExecuteCreateVertex(*client, 3); client->Execute("MATCH (n {id:2}), (m {id:3}) CALL write.create_edge(n, m, 'edge') YIELD e RETURN e"); client->DiscardAll(); CreateOnDeleteTriggers(*client, true, allowed_trigger_types); client->Execute("MATCH ()-[e]->() CALL write.delete_edge(e)"); client->DiscardAll(); client->Execute("MATCH (n {id:2}) CALL write.delete_vertex(n)"); client->DiscardAll(); auto number_of_expected_vertices = 1; for (const auto allowed_trigger_type : allowed_trigger_types) { switch (allowed_trigger_type) { case AllowedTriggerType::VERTEX: number_of_expected_vertices += 1; break; case AllowedTriggerType::EDGE: number_of_expected_vertices += 1; break; case AllowedTriggerType::OBJECT: number_of_expected_vertices += 2; break; } } CheckNumberOfAllVertices(*client, number_of_expected_vertices); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::EDGE), kTriggerDeletedEdgeLabel, 1); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::OBJECT), kTriggerDeletedObjectLabel, 1); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::VERTEX), kTriggerDeletedVertexLabel, 2); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::OBJECT), kTriggerDeletedObjectLabel, 2); DropOnDeleteTriggers(*client, allowed_trigger_types); client->Execute("MATCH (n) DETACH DELETE n;"); client->DiscardAll(); }; const auto run_delete_trigger_write_procedure_delete_detach_test = [&](const std::unordered_set &allowed_trigger_types) { ExecuteCreateVertex(*client, 2); ExecuteCreateVertex(*client, 3); client->Execute("MATCH (n {id:2}), (m {id:3}) CALL write.create_edge(n, m, 'edge') YIELD e RETURN e"); client->DiscardAll(); CreateOnDeleteTriggers(*client, true, allowed_trigger_types); client->Execute("MATCH (v {id:2}) CALL write.detach_delete_vertex(v)"); client->DiscardAll(); auto number_of_expected_vertices = 1; for (const auto allowed_trigger_type : allowed_trigger_types) { switch (allowed_trigger_type) { case AllowedTriggerType::VERTEX: number_of_expected_vertices += 1; break; case AllowedTriggerType::EDGE: number_of_expected_vertices += 1; break; case AllowedTriggerType::OBJECT: number_of_expected_vertices += 2; break; } } CheckNumberOfAllVertices(*client, number_of_expected_vertices); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::EDGE), kTriggerDeletedEdgeLabel, 1); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::OBJECT), kTriggerDeletedObjectLabel, 1); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::VERTEX), kTriggerDeletedVertexLabel, 2); ValidateVertexExistance(*client, allowed_trigger_types.contains(AllowedTriggerType::OBJECT), kTriggerDeletedObjectLabel, 2); DropOnDeleteTriggers(*client, allowed_trigger_types); client->Execute("MATCH (n) DETACH DELETE n;"); client->DiscardAll(); }; const auto run_for_trigger_combinations = [&](const bool is_before) { const std::array trigger_type_combinations{ std::unordered_set{AllowedTriggerType::VERTEX}, std::unordered_set{AllowedTriggerType::EDGE}, std::unordered_set{AllowedTriggerType::OBJECT}, std::unordered_set{AllowedTriggerType::VERTEX, AllowedTriggerType::EDGE}, std::unordered_set{AllowedTriggerType::VERTEX, AllowedTriggerType::OBJECT}, std::unordered_set{AllowedTriggerType::EDGE, AllowedTriggerType::OBJECT}, std::unordered_set{AllowedTriggerType::VERTEX, AllowedTriggerType::EDGE, AllowedTriggerType::OBJECT}, }; for (const auto &allowed_trigger_types : trigger_type_combinations) { run_delete_trigger_tests(is_before, allowed_trigger_types); run_delete_trigger_write_procedure_tests(allowed_trigger_types); run_delete_trigger_write_procedure_delete_detach_test(allowed_trigger_types); } }; static constexpr bool kBeforeCommit = true; static constexpr bool kAfterCommit = false; run_for_trigger_combinations(kBeforeCommit); run_for_trigger_combinations(kAfterCommit); return 0; }