diff --git a/CHANGELOG.md b/CHANGELOG.md index 95fac9381..e281fe50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Major Feature and Improvements +* Added triggers. * Replaced mg_client with mgconsole ### Bug Fixes diff --git a/src/query/CMakeLists.txt b/src/query/CMakeLists.txt index 5c692e44e..b41e6380c 100644 --- a/src/query/CMakeLists.txt +++ b/src/query/CMakeLists.txt @@ -33,6 +33,7 @@ set(mg_query_sources procedure/py_module.cpp serialization/property_value.cpp trigger.cpp + trigger_context.cpp typed_value.cpp) add_library(mg-query STATIC ${mg_query_sources}) diff --git a/src/query/interpreter.cpp b/src/query/interpreter.cpp index c67b5a91f..67fa4b279 100644 --- a/src/query/interpreter.cpp +++ b/src/query/interpreter.cpp @@ -606,58 +606,6 @@ InterpreterContext::InterpreterContext(storage::Storage *db, const std::filesyst Interpreter::Interpreter(InterpreterContext *interpreter_context) : interpreter_context_(interpreter_context) { MG_ASSERT(interpreter_context_, "Interpreter context must not be NULL"); - // try { - // { - // auto storage_acc = interpreter_context_->db->Access(); - // DbAccessor dba(&storage_acc); - // auto triggers_acc = interpreter_context_->before_commit_triggers.access(); - // triggers_acc.insert(Trigger{"BeforeDelete", - // "UNWIND deletedVertices as u CREATE(:DELETED_VERTEX {id: id(u) + 10})", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, - // TriggerEventType::VERTEX_DELETE}); - // triggers_acc.insert(Trigger{"BeforeUpdatePropertyi", - // "UNWIND assignedVertexProperties as u SET u.vertex.two = u.new", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, - // TriggerEventType::VERTEX_UPDATE}); - // triggers_acc.insert(Trigger{"BeforeDeleteEdge", "UNWIND deletedEdges as u CREATE(:DELETED_EDGE {id: id(u) +10}) - // ", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, - // TriggerEventType::EDGE_DELETE}); - // // triggers_acc.insert(Trigger{"BeforeDelete2", "UNWIND deletedEdges as u SET u.deleted = 0", - // // &interpreter_context_->ast_cache, &dba, - // // &interpreter_context_->antlr_lock}); - // triggers_acc.insert(Trigger{"BeforeDeleteProcedure", - // "CALL script.procedure('VERTEX_UPDATE', updatedVertices) YIELD * RETURN *", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, - // TriggerEventType::VERTEX_UPDATE}); - // triggers_acc.insert(Trigger{"BeforeCreator", "UNWIND createdVertices as u SET u.before = id(u) + 10", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, - // TriggerEventType::VERTEX_CREATE}); - // triggers_acc.insert(Trigger{"BeforeCreatorEdge", "UNWIND createdEdges as u SET u.before = id(u) + 10", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, - // TriggerEventType::EDGE_CREATE}); - // triggers_acc.insert(Trigger{"BeforeSetLabelProcedure", - // "CALL label.procedure('VERTEX_UPDATE', assignedVertexLabels) YIELD * RETURN *", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, - // TriggerEventType::VERTEX_UPDATE}); - // } - // { - // auto storage_acc = interpreter_context->db->Access(); - // DbAccessor dba(&storage_acc); - // auto triggers_acc = interpreter_context->after_commit_triggers.access(); - // triggers_acc.insert(Trigger{"AfterDelete", "UNWIND deletedVertices as u CREATE(:DELETED {id: u.id + 100})", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, - // TriggerEventType::VERTEX_DELETE}); - // triggers_acc.insert(Trigger{"AfterCreator", "UNWIND createdVertices as u SET u.after = u.id + 100", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, - // TriggerEventType::VERTEX_CREATE}); - // triggers_acc.insert(Trigger{ - // "AfterUpdateProcedure", "CALL script.procedure('UPDATE',updatedObjects) YIELD * RETURN *", - // &interpreter_context_->ast_cache, &dba, &interpreter_context_->antlr_lock, TriggerEventType::UPDATE}); - // } - // } catch (const utils::BasicException &e) { - // spdlog::critical("Failed to create a trigger because: {}", e.what()); - // } } PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper) { @@ -675,7 +623,7 @@ PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper) execution_db_accessor_.emplace(db_accessor_.get()); if (interpreter_context_->trigger_store->HasTriggers()) { - trigger_context_collector_.emplace(); + trigger_context_collector_.emplace(interpreter_context_->trigger_store->GetEventTypes()); } }; } else if (query_upper == "COMMIT") { @@ -1504,7 +1452,7 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string, execution_db_accessor_.emplace(db_accessor_.get()); if (utils::Downcast(parsed_query.query) && interpreter_context_->trigger_store->HasTriggers()) { - trigger_context_collector_.emplace(); + trigger_context_collector_.emplace(interpreter_context_->trigger_store->GetEventTypes()); } } @@ -1648,6 +1596,7 @@ void Interpreter::Commit() { std::optional trigger_context = std::nullopt; if (trigger_context_collector_) { trigger_context.emplace(std::move(*trigger_context_collector_).TransformToTriggerContext()); + trigger_context_collector_.reset(); } if (trigger_context) { @@ -1667,6 +1616,12 @@ void Interpreter::Commit() { SPDLOG_DEBUG("Finished executing before commit triggers"); } + const auto reset_necessary_members = [this]() { + execution_db_accessor_.reset(); + db_accessor_.reset(); + trigger_context_collector_.reset(); + }; + auto maybe_constraint_violation = db_accessor_->Commit(); if (maybe_constraint_violation.HasError()) { const auto &constraint_violation = maybe_constraint_violation.GetError(); @@ -1675,9 +1630,7 @@ void Interpreter::Commit() { auto label_name = execution_db_accessor_->LabelToName(constraint_violation.label); MG_ASSERT(constraint_violation.properties.size() == 1U); auto property_name = execution_db_accessor_->PropertyToName(*constraint_violation.properties.begin()); - execution_db_accessor_.reset(); - db_accessor_.reset(); - trigger_context_collector_.reset(); + reset_necessary_members(); throw QueryException("Unable to commit due to existence constraint violation on :{}({})", label_name, property_name); break; @@ -1688,9 +1641,7 @@ void Interpreter::Commit() { utils::PrintIterable( property_names_stream, constraint_violation.properties, ", ", [this](auto &stream, const auto &prop) { stream << execution_db_accessor_->PropertyToName(prop); }); - execution_db_accessor_.reset(); - db_accessor_.reset(); - trigger_context_collector_.reset(); + reset_necessary_members(); throw QueryException("Unable to commit due to unique constraint violation on :{}({})", label_name, property_names_stream.str()); break; @@ -1714,9 +1665,7 @@ void Interpreter::Commit() { }); } - execution_db_accessor_.reset(); - db_accessor_.reset(); - trigger_context_collector_.reset(); + reset_necessary_members(); SPDLOG_DEBUG("Finished comitting the transaction"); } diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 49698bc20..d29e980b8 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -1873,7 +1873,8 @@ bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) { throw QueryRuntimeException("Unexpected error when deleting a node."); } } - if (context.trigger_context_collector && res.GetValue()) { + if (context.trigger_context_collector && + context.trigger_context_collector->ShouldRegisterDeletedObject() && res.GetValue()) { context.trigger_context_collector->RegisterDeletedObject(res.GetValue()->first); for (const auto &deleted_edge : res.GetValue()->second) { context.trigger_context_collector->RegisterDeletedObject(deleted_edge); @@ -2025,6 +2026,9 @@ template void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetProperties::Op op, ExecutionContext *context) { std::optional> old_values; + const bool should_register_change = + context->trigger_context_collector && + context->trigger_context_collector->ShouldRegisterObjectPropertyChange(); if (op == SetProperties::Op::REPLACE) { auto maybe_value = record->ClearProperties(); if (maybe_value.HasError()) { @@ -2041,7 +2045,7 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr } } - if (context->trigger_context_collector) { + if (should_register_change) { old_values.emplace(std::move(*maybe_value)); } } @@ -2063,10 +2067,10 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr return *maybe_props; }; - auto register_set_property = [&](auto returned_old_value, auto key, auto new_value) { + auto register_set_property = [&](auto &&returned_old_value, auto key, auto &&new_value) { auto old_value = [&]() -> storage::PropertyValue { if (!old_values) { - return std::move(returned_old_value); + return std::forward(returned_old_value); } if (auto it = old_values->find(key); it != old_values->end()) { @@ -2075,8 +2079,9 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr return {}; }(); - context->trigger_context_collector->RegisterSetObjectProperty(*record, key, TypedValue(std::move(old_value)), - TypedValue(std::move(new_value))); + + context->trigger_context_collector->RegisterSetObjectProperty( + *record, key, TypedValue(std::move(old_value)), TypedValue(std::forward(new_value))); }; auto set_props = [&, record](auto properties) { @@ -2096,7 +2101,7 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr } } - if (context->trigger_context_collector) { + if (should_register_change) { register_set_property(std::move(*maybe_error), kv.first, std::move(kv.second)); } } @@ -2113,7 +2118,7 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr for (const auto &kv : rhs.ValueMap()) { auto key = context->db_accessor->NameToProperty(kv.first); auto old_value = PropsSetChecked(record, key, kv.second); - if (context->trigger_context_collector) { + if (should_register_change) { register_set_property(std::move(old_value), key, kv.second); } } @@ -2125,7 +2130,7 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr "map."); } - if (context->trigger_context_collector && old_values) { + if (should_register_change && old_values) { // register removed properties for (auto &[property_id, property_value] : *old_values) { context->trigger_context_collector->RegisterRemovedObjectProperty(*record, property_id, diff --git a/src/query/trigger.cpp b/src/query/trigger.cpp index 9f7edba84..3f4b0ccb4 100644 --- a/src/query/trigger.cpp +++ b/src/query/trigger.cpp @@ -14,7 +14,6 @@ namespace query { namespace { - auto IdentifierString(const TriggerIdentifierTag tag) noexcept { switch (tag) { case TriggerIdentifierTag::CREATED_VERTICES: @@ -36,10 +35,10 @@ auto IdentifierString(const TriggerIdentifierTag tag) noexcept { return "deletedObjects"; case TriggerIdentifierTag::SET_VERTEX_PROPERTIES: - return "assignedVertexProperties"; + return "setVertexProperties"; case TriggerIdentifierTag::SET_EDGE_PROPERTIES: - return "assignedEdgeProperties"; + return "setEdgeProperties"; case TriggerIdentifierTag::REMOVED_VERTEX_PROPERTIES: return "removedVertexProperties"; @@ -48,7 +47,7 @@ auto IdentifierString(const TriggerIdentifierTag tag) noexcept { return "removedEdgeProperties"; case TriggerIdentifierTag::SET_VERTEX_LABELS: - return "assignedVertexLabels"; + return "setVertexLabels"; case TriggerIdentifierTag::REMOVED_VERTEX_LABELS: return "removedVertexLabels"; @@ -87,7 +86,13 @@ std::vector> GetPredefinedIdentifier switch (event_type) { case EventType::ANY: - return {}; + return TagsToIdentifiers( + IdentifierTag::CREATED_VERTICES, IdentifierTag::CREATED_EDGES, IdentifierTag::CREATED_OBJECTS, + IdentifierTag::DELETED_VERTICES, IdentifierTag::DELETED_EDGES, IdentifierTag::DELETED_OBJECTS, + IdentifierTag::SET_VERTEX_PROPERTIES, IdentifierTag::SET_EDGE_PROPERTIES, + IdentifierTag::REMOVED_VERTEX_PROPERTIES, IdentifierTag::REMOVED_EDGE_PROPERTIES, + IdentifierTag::SET_VERTEX_LABELS, IdentifierTag::REMOVED_VERTEX_LABELS, IdentifierTag::UPDATED_VERTICES, + IdentifierTag::UPDATED_EDGES, IdentifierTag::UPDATED_OBJECTS); case EventType::CREATE: return TagsToIdentifiers(IdentifierTag::CREATED_OBJECTS); @@ -120,420 +125,7 @@ std::vector> GetPredefinedIdentifier IdentifierTag::UPDATED_EDGES); } } - -template -concept WithToMap = requires(const T value, DbAccessor *dba) { - { value.ToMap(dba) } - ->std::same_as>; -}; - -template -TypedValue ToTypedValue(const T &value, DbAccessor *dba) { - return TypedValue{value.ToMap(dba)}; -} - -template -TypedValue ToTypedValue(const detail::CreatedObject &created_object, [[maybe_unused]] DbAccessor *dba) { - return TypedValue{created_object.object}; -} - -template -TypedValue ToTypedValue(const detail::DeletedObject &deleted_object, [[maybe_unused]] DbAccessor *dba) { - return TypedValue{deleted_object.object}; -} - -template -concept WithIsValid = requires(const T value) { - { value.IsValid() } - ->std::same_as; -}; - -template -concept ConvertableToTypedValue = requires(T value, DbAccessor *dba) { - { ToTypedValue(value, dba) } - ->std::same_as; -} -&&WithIsValid; - -template -concept LabelUpdateContext = utils::SameAsAnyOf; - -template -TypedValue ToTypedValue(const std::vector &values, DbAccessor *dba) { - std::unordered_map> vertices_by_labels; - - for (const auto &value : values) { - if (value.IsValid()) { - vertices_by_labels[value.label_id].emplace_back(value.object); - } - } - - TypedValue result{std::vector{}}; - auto &typed_values = result.ValueList(); - for (auto &[label_id, vertices] : vertices_by_labels) { - typed_values.emplace_back(std::map{ - {std::string{"label"}, TypedValue(dba->LabelToName(label_id))}, - {std::string{"vertices"}, TypedValue(std::move(vertices))}, - }); - } - - return result; -} - -template -TypedValue ToTypedValue(const std::vector &values, DbAccessor *dba) requires(!LabelUpdateContext) { - TypedValue result{std::vector{}}; - auto &typed_values = result.ValueList(); - typed_values.reserve(values.size()); - - for (const auto &value : values) { - if (value.IsValid()) { - typed_values.push_back(ToTypedValue(value, dba)); - } - } - - return result; -} - -template -const char *TypeToString() { - if constexpr (std::same_as>) { - return "created_vertex"; - } else if constexpr (std::same_as>) { - return "created_edge"; - } else if constexpr (std::same_as>) { - return "deleted_vertex"; - } else if constexpr (std::same_as>) { - return "deleted_edge"; - } else if constexpr (std::same_as>) { - return "set_vertex_property"; - } else if constexpr (std::same_as>) { - return "set_edge_property"; - } else if constexpr (std::same_as>) { - return "removed_vertex_property"; - } else if constexpr (std::same_as>) { - return "removed_edge_property"; - } else if constexpr (std::same_as) { - return "set_vertex_label"; - } else if constexpr (std::same_as) { - return "removed_vertex_label"; - } -} - -template -concept ContextInfo = WithToMap &&WithIsValid; - -template -TypedValue Concatenate(DbAccessor *dba, const std::vector &...args) { - const auto size = (args.size() + ...); - TypedValue result{std::vector{}}; - auto &concatenated = result.ValueList(); - concatenated.reserve(size); - - const auto add_to_concatenated = [&](const std::vector &values) { - for (const auto &value : values) { - if (value.IsValid()) { - auto map = value.ToMap(dba); - map["event_type"] = TypeToString(); - concatenated.emplace_back(std::move(map)); - } - } - }; - - (add_to_concatenated(args), ...); - - return result; -} - -template -concept WithEmpty = requires(const T value) { - { value.empty() } - ->std::same_as; -}; - -template -bool AnyContainsValue(const TContainer &...value_containers) { - return (!value_containers.empty() || ...); -} } // namespace -namespace detail { -bool SetVertexLabel::IsValid() const { return object.IsVisible(storage::View::OLD); } - -std::map SetVertexLabel::ToMap(DbAccessor *dba) const { - return {{"vertex", TypedValue{object}}, {"label", TypedValue{dba->LabelToName(label_id)}}}; -} - -bool RemovedVertexLabel::IsValid() const { return object.IsVisible(storage::View::OLD); } - -std::map RemovedVertexLabel::ToMap(DbAccessor *dba) const { - return {{"vertex", TypedValue{object}}, {"label", TypedValue{dba->LabelToName(label_id)}}}; -} -} // namespace detail - -const char *TriggerEventTypeToString(const TriggerEventType event_type) { - switch (event_type) { - case TriggerEventType::ANY: - return "ANY"; - - case TriggerEventType::CREATE: - return "CREATE"; - - case TriggerEventType::VERTEX_CREATE: - return "() CREATE"; - - case TriggerEventType::EDGE_CREATE: - return "--> CREATE"; - - case TriggerEventType::DELETE: - return "DELETE"; - - case TriggerEventType::VERTEX_DELETE: - return "() DELETE"; - - case TriggerEventType::EDGE_DELETE: - return "--> DELETE"; - - case TriggerEventType::UPDATE: - return "UPDATE"; - - case TriggerEventType::VERTEX_UPDATE: - return "() UPDATE"; - - case TriggerEventType::EDGE_UPDATE: - return "--> UPDATE"; - } -} - -void TriggerContext::AdaptForAccessor(DbAccessor *accessor) { - { - // adapt created_vertices_ - auto it = created_vertices_.begin(); - for (auto &created_vertex : created_vertices_) { - if (auto maybe_vertex = accessor->FindVertex(created_vertex.object.Gid(), storage::View::OLD); maybe_vertex) { - *it = detail::CreatedObject{*maybe_vertex}; - ++it; - } - } - created_vertices_.erase(it, created_vertices_.end()); - } - - // deleted_vertices_ should keep the transaction context of the transaction which deleted it - // because no other transaction can modify an object after it's deleted so it should be the - // latest state of the object - - const auto adapt_context_with_vertex = [accessor](auto *values) { - auto it = values->begin(); - for (auto &value : *values) { - if (auto maybe_vertex = accessor->FindVertex(value.object.Gid(), storage::View::OLD); maybe_vertex) { - *it = std::move(value); - it->object = *maybe_vertex; - ++it; - } - } - values->erase(it, values->end()); - }; - - adapt_context_with_vertex(&set_vertex_properties_); - adapt_context_with_vertex(&removed_vertex_properties_); - adapt_context_with_vertex(&set_vertex_labels_); - adapt_context_with_vertex(&removed_vertex_labels_); - - { - // adapt created_edges - auto it = created_edges_.begin(); - for (auto &created_edge : created_edges_) { - const auto maybe_from_vertex = accessor->FindVertex(created_edge.object.From().Gid(), storage::View::OLD); - if (!maybe_from_vertex) { - continue; - } - auto maybe_out_edges = maybe_from_vertex->OutEdges(storage::View::OLD); - MG_ASSERT(maybe_out_edges.HasValue()); - const auto edge_gid = created_edge.object.Gid(); - for (const auto &edge : *maybe_out_edges) { - if (edge.Gid() == edge_gid) { - *it = detail::CreatedObject{edge}; - ++it; - } - } - } - created_edges_.erase(it, created_edges_.end()); - } - - // deleted_edges_ should keep the transaction context of the transaction which deleted it - // because no other transaction can modify an object after it's deleted so it should be the - // latest state of the object - - const auto adapt_context_with_edge = [accessor](auto *values) { - auto it = values->begin(); - for (const auto &value : *values) { - if (auto maybe_vertex = accessor->FindVertex(value.object.From().Gid(), storage::View::OLD); maybe_vertex) { - auto maybe_out_edges = maybe_vertex->OutEdges(storage::View::OLD); - MG_ASSERT(maybe_out_edges.HasValue()); - for (const auto &edge : *maybe_out_edges) { - if (edge.Gid() == value.object.Gid()) { - *it = std::move(value); - it->object = edge; - ++it; - break; - } - } - } - } - values->erase(it, values->end()); - }; - - adapt_context_with_edge(&set_edge_properties_); - adapt_context_with_edge(&removed_edge_properties_); -} - -TypedValue TriggerContext::GetTypedValue(const TriggerIdentifierTag tag, DbAccessor *dba) const { - switch (tag) { - case TriggerIdentifierTag::CREATED_VERTICES: - return ToTypedValue(created_vertices_, dba); - - case TriggerIdentifierTag::CREATED_EDGES: - return ToTypedValue(created_edges_, dba); - - case TriggerIdentifierTag::CREATED_OBJECTS: - return Concatenate(dba, created_vertices_, created_edges_); - - case TriggerIdentifierTag::DELETED_VERTICES: - return ToTypedValue(deleted_vertices_, dba); - - case TriggerIdentifierTag::DELETED_EDGES: - return ToTypedValue(deleted_edges_, dba); - - case TriggerIdentifierTag::DELETED_OBJECTS: - return Concatenate(dba, deleted_vertices_, deleted_edges_); - - case TriggerIdentifierTag::SET_VERTEX_PROPERTIES: - return ToTypedValue(set_vertex_properties_, dba); - - case TriggerIdentifierTag::SET_EDGE_PROPERTIES: - return ToTypedValue(set_edge_properties_, dba); - - case TriggerIdentifierTag::REMOVED_VERTEX_PROPERTIES: - return ToTypedValue(removed_vertex_properties_, dba); - - case TriggerIdentifierTag::REMOVED_EDGE_PROPERTIES: - return ToTypedValue(removed_edge_properties_, dba); - - case TriggerIdentifierTag::SET_VERTEX_LABELS: - return ToTypedValue(set_vertex_labels_, dba); - - case TriggerIdentifierTag::REMOVED_VERTEX_LABELS: - return ToTypedValue(removed_vertex_labels_, dba); - - case TriggerIdentifierTag::UPDATED_VERTICES: - return Concatenate(dba, set_vertex_properties_, removed_vertex_properties_, set_vertex_labels_, - removed_vertex_labels_); - - case TriggerIdentifierTag::UPDATED_EDGES: - return Concatenate(dba, set_edge_properties_, removed_edge_properties_); - - case TriggerIdentifierTag::UPDATED_OBJECTS: - return Concatenate(dba, set_vertex_properties_, set_edge_properties_, removed_vertex_properties_, - removed_edge_properties_, set_vertex_labels_, removed_vertex_labels_); - } -} - -bool TriggerContext::ShouldEventTrigger(const TriggerEventType event_type) const { - using EventType = TriggerEventType; - switch (event_type) { - case EventType::ANY: - return true; - - case EventType::CREATE: - return AnyContainsValue(created_vertices_, created_edges_); - - case EventType::VERTEX_CREATE: - return AnyContainsValue(created_vertices_); - - case EventType::EDGE_CREATE: - return AnyContainsValue(created_edges_); - - case EventType::DELETE: - return AnyContainsValue(deleted_vertices_, deleted_edges_); - - case EventType::VERTEX_DELETE: - return AnyContainsValue(deleted_vertices_); - - case EventType::EDGE_DELETE: - return AnyContainsValue(deleted_edges_); - - case EventType::UPDATE: - return AnyContainsValue(set_vertex_properties_, set_edge_properties_, removed_vertex_properties_, - removed_edge_properties_, set_vertex_labels_, removed_vertex_labels_); - - case EventType::VERTEX_UPDATE: - return AnyContainsValue(set_vertex_properties_, removed_vertex_properties_, set_vertex_labels_, - removed_vertex_labels_); - - case EventType::EDGE_UPDATE: - return AnyContainsValue(set_edge_properties_, removed_edge_properties_); - } -} - -void TriggerContextCollector::UpdateLabelMap(const VertexAccessor vertex, const storage::LabelId label_id, - const LabelChange change) { - auto ®istry = GetRegistry(); - if (registry.created_objects_.count(vertex.Gid())) { - return; - } - - if (auto it = label_changes_.find({vertex, label_id}); it != label_changes_.end()) { - it->second = std::clamp(it->second + LabelChangeToInt(change), -1, 1); - return; - } - - label_changes_.emplace(std::make_pair(vertex, label_id), LabelChangeToInt(change)); -} - -void TriggerContextCollector::RegisterSetVertexLabel(const VertexAccessor &vertex, const storage::LabelId label_id) { - UpdateLabelMap(vertex, label_id, LabelChange::ADD); -} - -void TriggerContextCollector::RegisterRemovedVertexLabel(const VertexAccessor &vertex, - const storage::LabelId label_id) { - UpdateLabelMap(vertex, label_id, LabelChange::REMOVE); -} - -int8_t TriggerContextCollector::LabelChangeToInt(LabelChange change) { - static_assert(std::is_same_v, int8_t>, - "The underlying type of LabelChange doesn't match the return type!"); - return static_cast(change); -} - -TriggerContext TriggerContextCollector::TransformToTriggerContext() && { - auto [created_vertices, deleted_vertices, set_vertex_properties, removed_vertex_properties] = - std::move(vertex_registry_).Summarize(); - auto [set_vertex_labels, removed_vertex_labels] = LabelMapToList(std::move(label_changes_)); - auto [created_edges, deleted_edges, set_edge_properties, removed_edge_properties] = - std::move(edge_registry_).Summarize(); - - return {std::move(created_vertices), std::move(deleted_vertices), - std::move(set_vertex_properties), std::move(removed_vertex_properties), - std::move(set_vertex_labels), std::move(removed_vertex_labels), - std::move(created_edges), std::move(deleted_edges), - std::move(set_edge_properties), std::move(removed_edge_properties)}; -} - -TriggerContextCollector::LabelChangesLists TriggerContextCollector::LabelMapToList(LabelChangesMap &&label_changes) { - std::vector set_vertex_labels; - std::vector removed_vertex_labels; - - for (const auto &[key, label_state] : label_changes) { - if (label_state == LabelChangeToInt(LabelChange::ADD)) { - set_vertex_labels.emplace_back(key.first, key.second); - } else if (label_state == LabelChangeToInt(LabelChange::REMOVE)) { - removed_vertex_labels.emplace_back(key.first, key.second); - } - } - - label_changes.clear(); - - return {std::move(set_vertex_labels), std::move(removed_vertex_labels)}; -} Trigger::Trigger(std::string name, const std::string &query, const std::map &user_parameters, @@ -777,4 +369,18 @@ std::vector TriggerStore::GetTriggerInfo() const { return info; } + +std::unordered_set TriggerStore::GetEventTypes() const { + std::unordered_set event_types; + + const auto add_event_types = [&](const utils::SkipList &trigger_list) { + for (const auto &trigger : trigger_list.access()) { + event_types.insert(trigger.EventType()); + } + }; + + add_event_types(before_commit_triggers_); + add_event_types(after_commit_triggers_); + return event_types; +} } // namespace query diff --git a/src/query/trigger.hpp b/src/query/trigger.hpp index a3e296aa0..1eb22bd08 100644 --- a/src/query/trigger.hpp +++ b/src/query/trigger.hpp @@ -1,364 +1,23 @@ #pragma once -#include -#include -#include -#include -#include + +#include +#include +#include +#include +#include #include +#include #include "kvstore/kvstore.hpp" #include "query/cypher_query_interpreter.hpp" +#include "query/db_accessor.hpp" #include "query/frontend/ast/ast.hpp" -#include "query/typed_value.hpp" +#include "query/trigger_context.hpp" #include "storage/v2/property_value.hpp" -#include "utils/concepts.hpp" -#include "utils/fnv.hpp" +#include "utils/skip_list.hpp" +#include "utils/spin_lock.hpp" namespace query { -namespace detail { -template -concept ObjectAccessor = utils::SameAsAnyOf; - -template -const char *ObjectString() { - if constexpr (std::same_as) { - return "vertex"; - } else { - return "edge"; - } -} - -template -struct CreatedObject { - explicit CreatedObject(const TAccessor &object) : object{object} {} - - bool IsValid() const { return object.IsVisible(storage::View::OLD); } - std::map ToMap([[maybe_unused]] DbAccessor *dba) const { - return {{ObjectString(), TypedValue{object}}}; - } - - TAccessor object; -}; - -template -struct DeletedObject { - explicit DeletedObject(const TAccessor &object) : object{object} {} - - bool IsValid() const { return object.IsVisible(storage::View::OLD); } - std::map ToMap([[maybe_unused]] DbAccessor *dba) const { - return {{ObjectString(), TypedValue{object}}}; - } - - TAccessor object; -}; - -template -struct SetObjectProperty { - explicit SetObjectProperty(const TAccessor &object, storage::PropertyId key, TypedValue old_value, - TypedValue new_value) - : object{object}, key{key}, old_value{std::move(old_value)}, new_value{std::move(new_value)} {} - - std::map ToMap(DbAccessor *dba) const { - return {{ObjectString(), TypedValue{object}}, - {"key", TypedValue{dba->PropertyToName(key)}}, - {"old", old_value}, - {"new", new_value}}; - } - - bool IsValid() const { return object.IsVisible(storage::View::OLD); } - - TAccessor object; - storage::PropertyId key; - TypedValue old_value; - TypedValue new_value; -}; - -template -struct RemovedObjectProperty { - explicit RemovedObjectProperty(const TAccessor &object, storage::PropertyId key, TypedValue old_value) - : object{object}, key{key}, old_value{std::move(old_value)} {} - - std::map ToMap(DbAccessor *dba) const { - return {{detail::ObjectString(), TypedValue{object}}, - {"key", TypedValue{dba->PropertyToName(key)}}, - {"old", old_value}}; - } - - bool IsValid() const { return object.IsVisible(storage::View::OLD); } - - TAccessor object; - storage::PropertyId key; - TypedValue old_value; -}; - -struct SetVertexLabel { - explicit SetVertexLabel(const VertexAccessor &vertex, const storage::LabelId label_id) - : object{vertex}, label_id{label_id} {} - - std::map ToMap(DbAccessor *dba) const; - bool IsValid() const; - - VertexAccessor object; - storage::LabelId label_id; -}; - -struct RemovedVertexLabel { - explicit RemovedVertexLabel(const VertexAccessor &vertex, const storage::LabelId label_id) - : object{vertex}, label_id{label_id} {} - - std::map ToMap(DbAccessor *dba) const; - bool IsValid() const; - - VertexAccessor object; - storage::LabelId label_id; -}; -} // namespace detail - -enum class TriggerIdentifierTag : uint8_t { - CREATED_VERTICES, - CREATED_EDGES, - CREATED_OBJECTS, - DELETED_VERTICES, - DELETED_EDGES, - DELETED_OBJECTS, - SET_VERTEX_PROPERTIES, - SET_EDGE_PROPERTIES, - REMOVED_VERTEX_PROPERTIES, - REMOVED_EDGE_PROPERTIES, - SET_VERTEX_LABELS, - REMOVED_VERTEX_LABELS, - UPDATED_VERTICES, - UPDATED_EDGES, - UPDATED_OBJECTS -}; - -enum class TriggerEventType : uint8_t { - ANY, // Triggers always - VERTEX_CREATE, - EDGE_CREATE, - CREATE, - VERTEX_DELETE, - EDGE_DELETE, - DELETE, - VERTEX_UPDATE, - EDGE_UPDATE, - UPDATE -}; - -const char *TriggerEventTypeToString(TriggerEventType event_type); - -static_assert(std::is_trivially_copy_constructible_v, - "VertexAccessor is not trivially copy constructible, move it where possible and remove this assert"); -static_assert(std::is_trivially_copy_constructible_v, - "EdgeAccessor is not trivially copy constructible, move it where possible and remove this asssert"); - -// Holds the information necessary for triggers -class TriggerContext { - public: - TriggerContext() = default; - TriggerContext(std::vector> created_vertices, - std::vector> deleted_vertices, - std::vector> set_vertex_properties, - std::vector> removed_vertex_properties, - std::vector set_vertex_labels, - std::vector removed_vertex_labels, - std::vector> created_edges, - std::vector> deleted_edges, - std::vector> set_edge_properties, - std::vector> removed_edge_properties) - : created_vertices_{std::move(created_vertices)}, - deleted_vertices_{std::move(deleted_vertices)}, - set_vertex_properties_{std::move(set_vertex_properties)}, - removed_vertex_properties_{std::move(removed_vertex_properties)}, - set_vertex_labels_{std::move(set_vertex_labels)}, - removed_vertex_labels_{std::move(removed_vertex_labels)}, - created_edges_{std::move(created_edges)}, - deleted_edges_{std::move(deleted_edges)}, - set_edge_properties_{std::move(set_edge_properties)}, - removed_edge_properties_{std::move(removed_edge_properties)} {} - TriggerContext(const TriggerContext &) = default; - TriggerContext(TriggerContext &&) = default; - TriggerContext &operator=(const TriggerContext &) = default; - TriggerContext &operator=(TriggerContext &&) = default; - - // Adapt the TriggerContext object inplace for a different DbAccessor - // (each derived accessor, e.g. VertexAccessor, gets adapted - // to the sent DbAccessor so they can be used safely) - void AdaptForAccessor(DbAccessor *accessor); - - // Get TypedValue for the identifier defined with tag - TypedValue GetTypedValue(TriggerIdentifierTag tag, DbAccessor *dba) const; - bool ShouldEventTrigger(TriggerEventType) const; - - private: - std::vector> created_vertices_; - std::vector> deleted_vertices_; - std::vector> set_vertex_properties_; - std::vector> removed_vertex_properties_; - std::vector set_vertex_labels_; - std::vector removed_vertex_labels_; - - std::vector> created_edges_; - std::vector> deleted_edges_; - std::vector> set_edge_properties_; - std::vector> removed_edge_properties_; -}; - -// Collects the information necessary for triggers during a single transaction run. -class TriggerContextCollector { - public: - template - void RegisterCreatedObject(const TAccessor &created_object) { - GetRegistry().created_objects_.emplace(created_object.Gid(), detail::CreatedObject{created_object}); - } - - template - void RegisterDeletedObject(const TAccessor &deleted_object) { - auto ®istry = GetRegistry(); - if (registry.created_objects_.count(deleted_object.Gid())) { - return; - } - - registry.deleted_objects_.emplace_back(deleted_object); - } - - template - void RegisterSetObjectProperty(const TAccessor &object, const storage::PropertyId key, TypedValue old_value, - TypedValue new_value) { - auto ®istry = GetRegistry(); - if (registry.created_objects_.count(object.Gid())) { - return; - } - - if (auto it = registry.property_changes_.find({object, key}); it != registry.property_changes_.end()) { - it->second.new_value = std::move(new_value); - return; - } - - registry.property_changes_.emplace(std::make_pair(object, key), - PropertyChangeInfo{std::move(old_value), std::move(new_value)}); - } - - template - void RegisterRemovedObjectProperty(const TAccessor &object, const storage::PropertyId key, TypedValue old_value) { - // property is already removed - if (old_value.IsNull()) { - return; - } - - RegisterSetObjectProperty(object, key, std::move(old_value), TypedValue()); - } - - void RegisterSetVertexLabel(const VertexAccessor &vertex, storage::LabelId label_id); - void RegisterRemovedVertexLabel(const VertexAccessor &vertex, storage::LabelId label_id); - [[nodiscard]] TriggerContext TransformToTriggerContext() &&; - - private: - struct HashPair { - template - size_t operator()(const std::pair &pair) const { - using GidType = decltype(std::declval().Gid()); - return utils::HashCombine{}(pair.first.Gid(), pair.second); - } - }; - - struct PropertyChangeInfo { - TypedValue old_value; - TypedValue new_value; - }; - - template - using PropertyChangesMap = - std::unordered_map, PropertyChangeInfo, HashPair>; - - template - using PropertyChangesLists = std::pair>, - std::vector>>; - - template - struct Registry { - using ChangesSummary = - std::tuple>, std::vector>, - std::vector>, - std::vector>>; - - [[nodiscard]] static PropertyChangesLists PropertyMapToList(PropertyChangesMap &&map) { - std::vector> set_object_properties; - std::vector> removed_object_properties; - - for (auto it = map.begin(); it != map.end(); it = map.erase(it)) { - const auto &[key, property_change_info] = *it; - if (property_change_info.old_value.IsNull() && property_change_info.new_value.IsNull()) { - // no change happened on the transaction level - continue; - } - - if (const auto is_equal = property_change_info.old_value == property_change_info.new_value; - is_equal.IsBool() && is_equal.ValueBool()) { - // no change happened on the transaction level - continue; - } - - if (property_change_info.new_value.IsNull()) { - removed_object_properties.emplace_back(key.first, key.second /* property_id */, - std::move(property_change_info.old_value)); - } else { - set_object_properties.emplace_back(key.first, key.second, std::move(property_change_info.old_value), - std::move(property_change_info.new_value)); - } - } - - return PropertyChangesLists{std::move(set_object_properties), std::move(removed_object_properties)}; - } - - [[nodiscard]] ChangesSummary Summarize() && { - auto [set_object_properties, removed_object_properties] = PropertyMapToList(std::move(property_changes_)); - std::vector> created_objects_vec; - created_objects_vec.reserve(created_objects_.size()); - std::transform(created_objects_.begin(), created_objects_.end(), std::back_inserter(created_objects_vec), - [](const auto &gid_and_created_object) { return gid_and_created_object.second; }); - created_objects_.clear(); - - return {std::move(created_objects_vec), std::move(deleted_objects_), std::move(set_object_properties), - std::move(removed_object_properties)}; - } - - std::unordered_map> created_objects_; - std::vector> deleted_objects_; - // During the transaction, a single property on a single object could be changed multiple times. - // We want to register only the global change, at the end of the transaction. The change consists of - // the value before the transaction start, and the latest value assigned throughout the transaction. - PropertyChangesMap property_changes_; - }; - - template - Registry &GetRegistry() { - if constexpr (std::same_as) { - return vertex_registry_; - } else { - return edge_registry_; - } - } - - using LabelChangesMap = std::unordered_map, int8_t, HashPair>; - using LabelChangesLists = std::pair, std::vector>; - - enum class LabelChange : int8_t { REMOVE = -1, ADD = 1 }; - - static int8_t LabelChangeToInt(LabelChange change); - - [[nodiscard]] static LabelChangesLists LabelMapToList(LabelChangesMap &&label_changes); - - void UpdateLabelMap(VertexAccessor vertex, storage::LabelId label_id, LabelChange change); - - Registry vertex_registry_; - Registry edge_registry_; - // During the transaction, a single label on a single vertex could be added and removed multiple times. - // We want to register only the global change, at the end of the transaction. The change consists of - // the state of the label before the transaction start, and the latest state assigned throughout the transaction. - LabelChangesMap label_changes_; -}; - struct Trigger { explicit Trigger(std::string name, const std::string &query, const std::map &user_parameters, TriggerEventType event_type, @@ -424,6 +83,7 @@ struct TriggerStore { const auto &AfterCommitTriggers() const noexcept { return after_commit_triggers_; } bool HasTriggers() const noexcept { return before_commit_triggers_.size() > 0 || after_commit_triggers_.size() > 0; } + std::unordered_set GetEventTypes() const; private: utils::SpinLock store_lock_; diff --git a/src/query/trigger_context.cpp b/src/query/trigger_context.cpp new file mode 100644 index 000000000..7daca97a5 --- /dev/null +++ b/src/query/trigger_context.cpp @@ -0,0 +1,549 @@ +#include "query/trigger.hpp" + +#include + +#include "query/context.hpp" +#include "query/cypher_query_interpreter.hpp" +#include "query/db_accessor.hpp" +#include "query/frontend/ast/ast.hpp" +#include "query/interpret/frame.hpp" +#include "query/serialization/property_value.hpp" +#include "query/typed_value.hpp" +#include "storage/v2/property_value.hpp" +#include "utils/memory.hpp" + +namespace query { +namespace { +template +concept WithToMap = requires(const T value, DbAccessor *dba) { + { value.ToMap(dba) } + ->std::same_as>; +}; + +template +TypedValue ToTypedValue(const T &value, DbAccessor *dba) { + return TypedValue{value.ToMap(dba)}; +} + +template +TypedValue ToTypedValue(const detail::CreatedObject &created_object, [[maybe_unused]] DbAccessor *dba) { + return TypedValue{created_object.object}; +} + +template +TypedValue ToTypedValue(const detail::DeletedObject &deleted_object, [[maybe_unused]] DbAccessor *dba) { + return TypedValue{deleted_object.object}; +} + +template +concept WithIsValid = requires(const T value) { + { value.IsValid() } + ->std::same_as; +}; + +template +concept ConvertableToTypedValue = requires(T value, DbAccessor *dba) { + { ToTypedValue(value, dba) } + ->std::same_as; +} +&&WithIsValid; + +template +concept LabelUpdateContext = utils::SameAsAnyOf; + +template +TypedValue ToTypedValue(const std::vector &values, DbAccessor *dba) { + std::unordered_map> vertices_by_labels; + + for (const auto &value : values) { + if (value.IsValid()) { + vertices_by_labels[value.label_id].emplace_back(value.object); + } + } + + TypedValue result{std::vector{}}; + auto &typed_values = result.ValueList(); + for (auto &[label_id, vertices] : vertices_by_labels) { + typed_values.emplace_back(std::map{ + {std::string{"label"}, TypedValue(dba->LabelToName(label_id))}, + {std::string{"vertices"}, TypedValue(std::move(vertices))}, + }); + } + + return result; +} + +template +TypedValue ToTypedValue(const std::vector &values, DbAccessor *dba) requires(!LabelUpdateContext) { + TypedValue result{std::vector{}}; + auto &typed_values = result.ValueList(); + typed_values.reserve(values.size()); + + for (const auto &value : values) { + if (value.IsValid()) { + typed_values.push_back(ToTypedValue(value, dba)); + } + } + + return result; +} + +template +const char *TypeToString() { + if constexpr (std::same_as>) { + return "created_vertex"; + } else if constexpr (std::same_as>) { + return "created_edge"; + } else if constexpr (std::same_as>) { + return "deleted_vertex"; + } else if constexpr (std::same_as>) { + return "deleted_edge"; + } else if constexpr (std::same_as>) { + return "set_vertex_property"; + } else if constexpr (std::same_as>) { + return "set_edge_property"; + } else if constexpr (std::same_as>) { + return "removed_vertex_property"; + } else if constexpr (std::same_as>) { + return "removed_edge_property"; + } else if constexpr (std::same_as) { + return "set_vertex_label"; + } else if constexpr (std::same_as) { + return "removed_vertex_label"; + } +} + +template +concept ContextInfo = WithToMap &&WithIsValid; + +template +TypedValue Concatenate(DbAccessor *dba, const std::vector &...args) { + const auto size = (args.size() + ...); + TypedValue result{std::vector{}}; + auto &concatenated = result.ValueList(); + concatenated.reserve(size); + + const auto add_to_concatenated = [&](const std::vector &values) { + for (const auto &value : values) { + if (value.IsValid()) { + auto map = value.ToMap(dba); + map["event_type"] = TypeToString(); + concatenated.emplace_back(std::move(map)); + } + } + }; + + (add_to_concatenated(args), ...); + + return result; +} + +template +concept WithEmpty = requires(const T value) { + { value.empty() } + ->std::same_as; +}; + +template +bool AnyContainsValue(const TContainer &...value_containers) { + return (!value_containers.empty() || ...); +} + +template +using ChangesSummary = + std::tuple>, std::vector>, + std::vector>, + std::vector>>; + +template +using PropertyChangesLists = + std::pair>, std::vector>>; + +template +[[nodiscard]] PropertyChangesLists PropertyMapToList( + query::TriggerContextCollector::PropertyChangesMap &&map) { + std::vector> set_object_properties; + std::vector> removed_object_properties; + + for (auto it = map.begin(); it != map.end(); it = map.erase(it)) { + const auto &[key, property_change_info] = *it; + if (property_change_info.old_value.IsNull() && property_change_info.new_value.IsNull()) { + // no change happened on the transaction level + continue; + } + + if (const auto is_equal = property_change_info.old_value == property_change_info.new_value; + is_equal.IsBool() && is_equal.ValueBool()) { + // no change happened on the transaction level + continue; + } + + if (property_change_info.new_value.IsNull()) { + removed_object_properties.emplace_back(key.first, key.second /* property_id */, + std::move(property_change_info.old_value)); + } else { + set_object_properties.emplace_back(key.first, key.second, std::move(property_change_info.old_value), + std::move(property_change_info.new_value)); + } + } + + return PropertyChangesLists{std::move(set_object_properties), std::move(removed_object_properties)}; +} + +template +[[nodiscard]] ChangesSummary Summarize(query::TriggerContextCollector::Registry &®istry) { + auto [set_object_properties, removed_object_properties] = PropertyMapToList(std::move(registry.property_changes)); + std::vector> created_objects_vec; + created_objects_vec.reserve(registry.created_objects.size()); + std::transform(registry.created_objects.begin(), registry.created_objects.end(), + std::back_inserter(created_objects_vec), + [](const auto &gid_and_created_object) { return gid_and_created_object.second; }); + registry.created_objects.clear(); + + return {std::move(created_objects_vec), std::move(registry.deleted_objects), std::move(set_object_properties), + std::move(removed_object_properties)}; +} +} // namespace + +namespace detail { +bool SetVertexLabel::IsValid() const { return object.IsVisible(storage::View::OLD); } + +std::map SetVertexLabel::ToMap(DbAccessor *dba) const { + return {{"vertex", TypedValue{object}}, {"label", TypedValue{dba->LabelToName(label_id)}}}; +} + +bool RemovedVertexLabel::IsValid() const { return object.IsVisible(storage::View::OLD); } + +std::map RemovedVertexLabel::ToMap(DbAccessor *dba) const { + return {{"vertex", TypedValue{object}}, {"label", TypedValue{dba->LabelToName(label_id)}}}; +} +} // namespace detail + +const char *TriggerEventTypeToString(const TriggerEventType event_type) { + switch (event_type) { + case TriggerEventType::ANY: + return "ANY"; + + case TriggerEventType::CREATE: + return "CREATE"; + + case TriggerEventType::VERTEX_CREATE: + return "() CREATE"; + + case TriggerEventType::EDGE_CREATE: + return "--> CREATE"; + + case TriggerEventType::DELETE: + return "DELETE"; + + case TriggerEventType::VERTEX_DELETE: + return "() DELETE"; + + case TriggerEventType::EDGE_DELETE: + return "--> DELETE"; + + case TriggerEventType::UPDATE: + return "UPDATE"; + + case TriggerEventType::VERTEX_UPDATE: + return "() UPDATE"; + + case TriggerEventType::EDGE_UPDATE: + return "--> UPDATE"; + } +} + +void TriggerContext::AdaptForAccessor(DbAccessor *accessor) { + { + // adapt created_vertices_ + auto it = created_vertices_.begin(); + for (auto &created_vertex : created_vertices_) { + if (auto maybe_vertex = accessor->FindVertex(created_vertex.object.Gid(), storage::View::OLD); maybe_vertex) { + *it = detail::CreatedObject{*maybe_vertex}; + ++it; + } + } + created_vertices_.erase(it, created_vertices_.end()); + } + + // deleted_vertices_ should keep the transaction context of the transaction which deleted it + // because no other transaction can modify an object after it's deleted so it should be the + // latest state of the object + + const auto adapt_context_with_vertex = [accessor](auto *values) { + auto it = values->begin(); + for (auto &value : *values) { + if (auto maybe_vertex = accessor->FindVertex(value.object.Gid(), storage::View::OLD); maybe_vertex) { + *it = std::move(value); + it->object = *maybe_vertex; + ++it; + } + } + values->erase(it, values->end()); + }; + + adapt_context_with_vertex(&set_vertex_properties_); + adapt_context_with_vertex(&removed_vertex_properties_); + adapt_context_with_vertex(&set_vertex_labels_); + adapt_context_with_vertex(&removed_vertex_labels_); + + { + // adapt created_edges + auto it = created_edges_.begin(); + for (auto &created_edge : created_edges_) { + const auto maybe_from_vertex = accessor->FindVertex(created_edge.object.From().Gid(), storage::View::OLD); + if (!maybe_from_vertex) { + continue; + } + auto maybe_out_edges = maybe_from_vertex->OutEdges(storage::View::OLD); + MG_ASSERT(maybe_out_edges.HasValue()); + const auto edge_gid = created_edge.object.Gid(); + for (const auto &edge : *maybe_out_edges) { + if (edge.Gid() == edge_gid) { + *it = detail::CreatedObject{edge}; + ++it; + } + } + } + created_edges_.erase(it, created_edges_.end()); + } + + // deleted_edges_ should keep the transaction context of the transaction which deleted it + // because no other transaction can modify an object after it's deleted so it should be the + // latest state of the object + + const auto adapt_context_with_edge = [accessor](auto *values) { + auto it = values->begin(); + for (const auto &value : *values) { + if (auto maybe_vertex = accessor->FindVertex(value.object.From().Gid(), storage::View::OLD); maybe_vertex) { + auto maybe_out_edges = maybe_vertex->OutEdges(storage::View::OLD); + MG_ASSERT(maybe_out_edges.HasValue()); + for (const auto &edge : *maybe_out_edges) { + if (edge.Gid() == value.object.Gid()) { + *it = std::move(value); + it->object = edge; + ++it; + break; + } + } + } + } + values->erase(it, values->end()); + }; + + adapt_context_with_edge(&set_edge_properties_); + adapt_context_with_edge(&removed_edge_properties_); +} + +TypedValue TriggerContext::GetTypedValue(const TriggerIdentifierTag tag, DbAccessor *dba) const { + switch (tag) { + case TriggerIdentifierTag::CREATED_VERTICES: + return ToTypedValue(created_vertices_, dba); + + case TriggerIdentifierTag::CREATED_EDGES: + return ToTypedValue(created_edges_, dba); + + case TriggerIdentifierTag::CREATED_OBJECTS: + return Concatenate(dba, created_vertices_, created_edges_); + + case TriggerIdentifierTag::DELETED_VERTICES: + return ToTypedValue(deleted_vertices_, dba); + + case TriggerIdentifierTag::DELETED_EDGES: + return ToTypedValue(deleted_edges_, dba); + + case TriggerIdentifierTag::DELETED_OBJECTS: + return Concatenate(dba, deleted_vertices_, deleted_edges_); + + case TriggerIdentifierTag::SET_VERTEX_PROPERTIES: + return ToTypedValue(set_vertex_properties_, dba); + + case TriggerIdentifierTag::SET_EDGE_PROPERTIES: + return ToTypedValue(set_edge_properties_, dba); + + case TriggerIdentifierTag::REMOVED_VERTEX_PROPERTIES: + return ToTypedValue(removed_vertex_properties_, dba); + + case TriggerIdentifierTag::REMOVED_EDGE_PROPERTIES: + return ToTypedValue(removed_edge_properties_, dba); + + case TriggerIdentifierTag::SET_VERTEX_LABELS: + return ToTypedValue(set_vertex_labels_, dba); + + case TriggerIdentifierTag::REMOVED_VERTEX_LABELS: + return ToTypedValue(removed_vertex_labels_, dba); + + case TriggerIdentifierTag::UPDATED_VERTICES: + return Concatenate(dba, set_vertex_properties_, removed_vertex_properties_, set_vertex_labels_, + removed_vertex_labels_); + + case TriggerIdentifierTag::UPDATED_EDGES: + return Concatenate(dba, set_edge_properties_, removed_edge_properties_); + + case TriggerIdentifierTag::UPDATED_OBJECTS: + return Concatenate(dba, set_vertex_properties_, set_edge_properties_, removed_vertex_properties_, + removed_edge_properties_, set_vertex_labels_, removed_vertex_labels_); + } +} + +bool TriggerContext::ShouldEventTrigger(const TriggerEventType event_type) const { + using EventType = TriggerEventType; + switch (event_type) { + case EventType::ANY: + return AnyContainsValue(created_vertices_, created_edges_, deleted_vertices_, deleted_edges_, + set_vertex_properties_, set_edge_properties_, removed_vertex_properties_, + removed_edge_properties_, set_vertex_labels_, removed_vertex_labels_); + + case EventType::CREATE: + return AnyContainsValue(created_vertices_, created_edges_); + + case EventType::VERTEX_CREATE: + return AnyContainsValue(created_vertices_); + + case EventType::EDGE_CREATE: + return AnyContainsValue(created_edges_); + + case EventType::DELETE: + return AnyContainsValue(deleted_vertices_, deleted_edges_); + + case EventType::VERTEX_DELETE: + return AnyContainsValue(deleted_vertices_); + + case EventType::EDGE_DELETE: + return AnyContainsValue(deleted_edges_); + + case EventType::UPDATE: + return AnyContainsValue(set_vertex_properties_, set_edge_properties_, removed_vertex_properties_, + removed_edge_properties_, set_vertex_labels_, removed_vertex_labels_); + + case EventType::VERTEX_UPDATE: + return AnyContainsValue(set_vertex_properties_, removed_vertex_properties_, set_vertex_labels_, + removed_vertex_labels_); + + case EventType::EDGE_UPDATE: + return AnyContainsValue(set_edge_properties_, removed_edge_properties_); + } +} + +void TriggerContextCollector::UpdateLabelMap(const VertexAccessor vertex, const storage::LabelId label_id, + const LabelChange change) { + auto ®istry = GetRegistry(); + if (!registry.should_register_updated_objects || registry.created_objects.count(vertex.Gid())) { + return; + } + + if (auto it = label_changes_.find({vertex, label_id}); it != label_changes_.end()) { + it->second = std::clamp(it->second + LabelChangeToInt(change), -1, 1); + return; + } + + label_changes_.emplace(std::make_pair(vertex, label_id), LabelChangeToInt(change)); +} + +TriggerContextCollector::TriggerContextCollector(const std::unordered_set &event_types) { + for (const auto event_type : event_types) { + switch (event_type) { + case TriggerEventType::ANY: + vertex_registry_.should_register_created_objects = true; + edge_registry_.should_register_created_objects = true; + vertex_registry_.should_register_deleted_objects = true; + edge_registry_.should_register_deleted_objects = true; + vertex_registry_.should_register_updated_objects = true; + edge_registry_.should_register_updated_objects = true; + break; + case TriggerEventType::VERTEX_CREATE: + vertex_registry_.should_register_created_objects = true; + break; + case TriggerEventType::EDGE_CREATE: + edge_registry_.should_register_created_objects = true; + break; + case TriggerEventType::CREATE: + vertex_registry_.should_register_created_objects = true; + edge_registry_.should_register_created_objects = true; + break; + case TriggerEventType::VERTEX_DELETE: + vertex_registry_.should_register_deleted_objects = true; + break; + case TriggerEventType::EDGE_DELETE: + edge_registry_.should_register_deleted_objects = true; + break; + case TriggerEventType::DELETE: + vertex_registry_.should_register_deleted_objects = true; + edge_registry_.should_register_deleted_objects = true; + break; + case TriggerEventType::VERTEX_UPDATE: + vertex_registry_.should_register_updated_objects = true; + break; + case TriggerEventType::EDGE_UPDATE: + edge_registry_.should_register_updated_objects = true; + break; + case TriggerEventType::UPDATE: + vertex_registry_.should_register_updated_objects = true; + edge_registry_.should_register_updated_objects = true; + break; + } + } + + const auto deduce_if_should_register_created = [](auto ®istry) { + // Registering the created objects is necessary to: + // - eliminate deleted objects that were created in the same transaction + // - eliminate set/removed properties and labels of newly created objects + // because those changes are only relevant for objects that have existed before the transaction. + registry.should_register_created_objects |= + registry.should_register_updated_objects || registry.should_register_deleted_objects; + }; + + deduce_if_should_register_created(vertex_registry_); + deduce_if_should_register_created(edge_registry_); +} + +bool TriggerContextCollector::ShouldRegisterVertexLabelChange() const { + return vertex_registry_.should_register_updated_objects; +} + +void TriggerContextCollector::RegisterSetVertexLabel(const VertexAccessor &vertex, const storage::LabelId label_id) { + UpdateLabelMap(vertex, label_id, LabelChange::ADD); +} + +void TriggerContextCollector::RegisterRemovedVertexLabel(const VertexAccessor &vertex, + const storage::LabelId label_id) { + UpdateLabelMap(vertex, label_id, LabelChange::REMOVE); +} + +int8_t TriggerContextCollector::LabelChangeToInt(LabelChange change) { + static_assert(std::is_same_v, int8_t>, + "The underlying type of LabelChange doesn't match the return type!"); + return static_cast(change); +} + +TriggerContext TriggerContextCollector::TransformToTriggerContext() && { + auto [created_vertices, deleted_vertices, set_vertex_properties, removed_vertex_properties] = + Summarize(std::move(vertex_registry_)); + auto [set_vertex_labels, removed_vertex_labels] = LabelMapToList(std::move(label_changes_)); + auto [created_edges, deleted_edges, set_edge_properties, removed_edge_properties] = + Summarize(std::move(edge_registry_)); + + return {std::move(created_vertices), std::move(deleted_vertices), + std::move(set_vertex_properties), std::move(removed_vertex_properties), + std::move(set_vertex_labels), std::move(removed_vertex_labels), + std::move(created_edges), std::move(deleted_edges), + std::move(set_edge_properties), std::move(removed_edge_properties)}; +} + +TriggerContextCollector::LabelChangesLists TriggerContextCollector::LabelMapToList(LabelChangesMap &&label_changes) { + std::vector set_vertex_labels; + std::vector removed_vertex_labels; + + for (const auto &[key, label_state] : label_changes) { + if (label_state == LabelChangeToInt(LabelChange::ADD)) { + set_vertex_labels.emplace_back(key.first, key.second); + } else if (label_state == LabelChangeToInt(LabelChange::REMOVE)) { + removed_vertex_labels.emplace_back(key.first, key.second); + } + } + + label_changes.clear(); + + return {std::move(set_vertex_labels), std::move(removed_vertex_labels)}; +} +} // namespace query diff --git a/src/query/trigger_context.hpp b/src/query/trigger_context.hpp new file mode 100644 index 000000000..8e985e6ce --- /dev/null +++ b/src/query/trigger_context.hpp @@ -0,0 +1,353 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "query/db_accessor.hpp" +#include "query/typed_value.hpp" +#include "storage/v2/property_value.hpp" +#include "storage/v2/view.hpp" +#include "utils/concepts.hpp" +#include "utils/fnv.hpp" + +namespace query { +namespace detail { +template +concept ObjectAccessor = utils::SameAsAnyOf; + +template +const char *ObjectString() { + if constexpr (std::same_as) { + return "vertex"; + } else { + return "edge"; + } +} + +template +struct CreatedObject { + explicit CreatedObject(const TAccessor &object) : object{object} {} + + bool IsValid() const { return object.IsVisible(storage::View::OLD); } + std::map ToMap([[maybe_unused]] DbAccessor *dba) const { + return {{ObjectString(), TypedValue{object}}}; + } + + TAccessor object; +}; + +template +struct DeletedObject { + explicit DeletedObject(const TAccessor &object) : object{object} {} + + bool IsValid() const { return object.IsVisible(storage::View::OLD); } + std::map ToMap([[maybe_unused]] DbAccessor *dba) const { + return {{ObjectString(), TypedValue{object}}}; + } + + TAccessor object; +}; + +template +struct SetObjectProperty { + explicit SetObjectProperty(const TAccessor &object, storage::PropertyId key, TypedValue old_value, + TypedValue new_value) + : object{object}, key{key}, old_value{std::move(old_value)}, new_value{std::move(new_value)} {} + + std::map ToMap(DbAccessor *dba) const { + return {{ObjectString(), TypedValue{object}}, + {"key", TypedValue{dba->PropertyToName(key)}}, + {"old", old_value}, + {"new", new_value}}; + } + + bool IsValid() const { return object.IsVisible(storage::View::OLD); } + + TAccessor object; + storage::PropertyId key; + TypedValue old_value; + TypedValue new_value; +}; + +template +struct RemovedObjectProperty { + explicit RemovedObjectProperty(const TAccessor &object, storage::PropertyId key, TypedValue old_value) + : object{object}, key{key}, old_value{std::move(old_value)} {} + + std::map ToMap(DbAccessor *dba) const { + return {{ObjectString(), TypedValue{object}}, + {"key", TypedValue{dba->PropertyToName(key)}}, + {"old", old_value}}; + } + + bool IsValid() const { return object.IsVisible(storage::View::OLD); } + + TAccessor object; + storage::PropertyId key; + TypedValue old_value; +}; + +struct SetVertexLabel { + explicit SetVertexLabel(const VertexAccessor &vertex, const storage::LabelId label_id) + : object{vertex}, label_id{label_id} {} + + std::map ToMap(DbAccessor *dba) const; + bool IsValid() const; + + VertexAccessor object; + storage::LabelId label_id; +}; + +struct RemovedVertexLabel { + explicit RemovedVertexLabel(const VertexAccessor &vertex, const storage::LabelId label_id) + : object{vertex}, label_id{label_id} {} + + std::map ToMap(DbAccessor *dba) const; + bool IsValid() const; + + VertexAccessor object; + storage::LabelId label_id; +}; +} // namespace detail + +enum class TriggerIdentifierTag : uint8_t { + CREATED_VERTICES, + CREATED_EDGES, + CREATED_OBJECTS, + DELETED_VERTICES, + DELETED_EDGES, + DELETED_OBJECTS, + SET_VERTEX_PROPERTIES, + SET_EDGE_PROPERTIES, + REMOVED_VERTEX_PROPERTIES, + REMOVED_EDGE_PROPERTIES, + SET_VERTEX_LABELS, + REMOVED_VERTEX_LABELS, + UPDATED_VERTICES, + UPDATED_EDGES, + UPDATED_OBJECTS +}; + +enum class TriggerEventType : uint8_t { + ANY, // Triggers on any change + VERTEX_CREATE, + EDGE_CREATE, + CREATE, + VERTEX_DELETE, + EDGE_DELETE, + DELETE, + VERTEX_UPDATE, + EDGE_UPDATE, + UPDATE +}; + +const char *TriggerEventTypeToString(TriggerEventType event_type); + +static_assert(std::is_trivially_copy_constructible_v, + "VertexAccessor is not trivially copy constructible, move it where possible and remove this assert"); +static_assert(std::is_trivially_copy_constructible_v, + "EdgeAccessor is not trivially copy constructible, move it where possible and remove this asssert"); + +// Holds the information necessary for triggers +class TriggerContext { + public: + TriggerContext() = default; + TriggerContext(std::vector> created_vertices, + std::vector> deleted_vertices, + std::vector> set_vertex_properties, + std::vector> removed_vertex_properties, + std::vector set_vertex_labels, + std::vector removed_vertex_labels, + std::vector> created_edges, + std::vector> deleted_edges, + std::vector> set_edge_properties, + std::vector> removed_edge_properties) + : created_vertices_{std::move(created_vertices)}, + deleted_vertices_{std::move(deleted_vertices)}, + set_vertex_properties_{std::move(set_vertex_properties)}, + removed_vertex_properties_{std::move(removed_vertex_properties)}, + set_vertex_labels_{std::move(set_vertex_labels)}, + removed_vertex_labels_{std::move(removed_vertex_labels)}, + created_edges_{std::move(created_edges)}, + deleted_edges_{std::move(deleted_edges)}, + set_edge_properties_{std::move(set_edge_properties)}, + removed_edge_properties_{std::move(removed_edge_properties)} {} + TriggerContext(const TriggerContext &) = default; + TriggerContext(TriggerContext &&) = default; + TriggerContext &operator=(const TriggerContext &) = default; + TriggerContext &operator=(TriggerContext &&) = default; + + // Adapt the TriggerContext object inplace for a different DbAccessor + // (each derived accessor, e.g. VertexAccessor, gets adapted + // to the sent DbAccessor so they can be used safely) + void AdaptForAccessor(DbAccessor *accessor); + + // Get TypedValue for the identifier defined with tag + TypedValue GetTypedValue(TriggerIdentifierTag tag, DbAccessor *dba) const; + bool ShouldEventTrigger(TriggerEventType) const; + + private: + std::vector> created_vertices_; + std::vector> deleted_vertices_; + std::vector> set_vertex_properties_; + std::vector> removed_vertex_properties_; + std::vector set_vertex_labels_; + std::vector removed_vertex_labels_; + + std::vector> created_edges_; + std::vector> deleted_edges_; + std::vector> set_edge_properties_; + std::vector> removed_edge_properties_; +}; + +// Collects the information necessary for triggers during a single transaction run. +class TriggerContextCollector { + public: + struct HashPairWithAccessor { + template + size_t operator()(const std::pair &pair) const { + using GidType = decltype(std::declval().Gid()); + return utils::HashCombine{}(pair.first.Gid(), pair.second); + } + }; + + struct PropertyChangeInfo { + TypedValue old_value; + TypedValue new_value; + }; + + template + using PropertyChangesMap = + std::unordered_map, PropertyChangeInfo, HashPairWithAccessor>; + + template + struct Registry { + bool should_register_created_objects{false}; + bool should_register_deleted_objects{false}; + bool should_register_updated_objects{false}; // Set/removed properties (and labels for vertices) + std::unordered_map> created_objects; + std::vector> deleted_objects; + // During the transaction, a single property on a single object could be changed multiple times. + // We want to register only the global change, at the end of the transaction. The change consists of + // the value before the transaction start, and the latest value assigned throughout the transaction. + PropertyChangesMap property_changes; + }; + + explicit TriggerContextCollector(const std::unordered_set &event_types); + TriggerContextCollector(const TriggerContextCollector &) = default; + TriggerContextCollector(TriggerContextCollector &&) = default; + TriggerContextCollector &operator=(const TriggerContextCollector &) = default; + TriggerContextCollector &operator=(TriggerContextCollector &&) = default; + ~TriggerContextCollector() = default; + + template + bool ShouldRegisterCreatedObject() const { + return GetRegistry().should_register_created_objects; + } + + template + void RegisterCreatedObject(const TAccessor &created_object) { + auto ®istry = GetRegistry(); + if (!registry.should_register_created_objects) { + return; + } + registry.created_objects.emplace(created_object.Gid(), detail::CreatedObject{created_object}); + } + + template + bool ShouldRegisterDeletedObject() const { + return GetRegistry().should_register_deleted_objects; + } + + template + void RegisterDeletedObject(const TAccessor &deleted_object) { + auto ®istry = GetRegistry(); + if (!registry.should_register_deleted_objects || registry.created_objects.count(deleted_object.Gid())) { + return; + } + + registry.deleted_objects.emplace_back(deleted_object); + } + + template + bool ShouldRegisterObjectPropertyChange() const { + return GetRegistry().should_register_updated_objects; + } + + template + void RegisterSetObjectProperty(const TAccessor &object, const storage::PropertyId key, TypedValue old_value, + TypedValue new_value) { + auto ®istry = GetRegistry(); + if (!registry.should_register_updated_objects) { + return; + } + + if (registry.created_objects.count(object.Gid())) { + return; + } + + if (auto it = registry.property_changes.find({object, key}); it != registry.property_changes.end()) { + it->second.new_value = std::move(new_value); + return; + } + + registry.property_changes.emplace(std::make_pair(object, key), + PropertyChangeInfo{std::move(old_value), std::move(new_value)}); + } + + template + void RegisterRemovedObjectProperty(const TAccessor &object, const storage::PropertyId key, TypedValue old_value) { + // property is already removed + if (old_value.IsNull()) { + return; + } + + RegisterSetObjectProperty(object, key, std::move(old_value), TypedValue()); + } + + bool ShouldRegisterVertexLabelChange() const; + void RegisterSetVertexLabel(const VertexAccessor &vertex, storage::LabelId label_id); + void RegisterRemovedVertexLabel(const VertexAccessor &vertex, storage::LabelId label_id); + [[nodiscard]] TriggerContext TransformToTriggerContext() &&; + + private: + template + const Registry &GetRegistry() const { + if constexpr (std::same_as) { + return vertex_registry_; + } else { + return edge_registry_; + } + } + + template + Registry &GetRegistry() { + return const_cast &>( + const_cast(this)->GetRegistry()); + } + + using LabelChangesMap = std::unordered_map, int8_t, HashPairWithAccessor>; + using LabelChangesLists = std::pair, std::vector>; + + enum class LabelChange : int8_t { REMOVE = -1, ADD = 1 }; + + static int8_t LabelChangeToInt(LabelChange change); + + [[nodiscard]] static LabelChangesLists LabelMapToList(LabelChangesMap &&label_changes); + + void UpdateLabelMap(VertexAccessor vertex, storage::LabelId label_id, LabelChange change); + + Registry vertex_registry_; + Registry edge_registry_; + // During the transaction, a single label on a single vertex could be added and removed multiple times. + // We want to register only the global change, at the end of the transaction. The change consists of + // the state of the label before the transaction start, and the latest state assigned throughout the transaction. + LabelChangesMap label_changes_; +}; +} // namespace query diff --git a/tests/e2e/triggers/on_update_triggers.cpp b/tests/e2e/triggers/on_update_triggers.cpp index 75da49ac8..3d24b1d28 100644 --- a/tests/e2e/triggers/on_update_triggers.cpp +++ b/tests/e2e/triggers/on_update_triggers.cpp @@ -105,7 +105,7 @@ void CreateOnUpdateTriggers(mg::Client &client, bool is_before) { fmt::format("CREATE TRIGGER SetVertexPropertiesTrigger ON () UPDATE " "{} COMMIT " "EXECUTE " - "UNWIND assignedVertexProperties as assignedVertexProperty " + "UNWIND setVertexProperties as assignedVertexProperty " "CREATE (n: {} {{ id: assignedVertexProperty.vertex.id }})", before_or_after, kTriggerSetVertexPropertyLabel)); client.DiscardAll(); @@ -121,7 +121,7 @@ void CreateOnUpdateTriggers(mg::Client &client, bool is_before) { fmt::format("CREATE TRIGGER SetVertexLabelsTrigger ON () UPDATE " "{} COMMIT " "EXECUTE " - "UNWIND assignedVertexLabels as assignedVertexLabel " + "UNWIND setVertexLabels as assignedVertexLabel " "UNWIND assignedVertexLabel.vertices as vertex " "CREATE (n: {} {{ id: vertex.id }})", before_or_after, kTriggerSetVertexLabelLabel)); @@ -140,7 +140,7 @@ void CreateOnUpdateTriggers(mg::Client &client, bool is_before) { fmt::format("CREATE TRIGGER SetEdgePropertiesTrigger ON --> UPDATE " "{} COMMIT " "EXECUTE " - "UNWIND assignedEdgeProperties as assignedEdgeProperty " + "UNWIND setEdgeProperties as assignedEdgeProperty " "CREATE (n: {} {{ id: assignedEdgeProperty.edge.id }})", before_or_after, kTriggerSetEdgePropertyLabel)); client.DiscardAll(); diff --git a/tests/unit/query_trigger.cpp b/tests/unit/query_trigger.cpp index e14822208..3a6ca4207 100644 --- a/tests/unit/query_trigger.cpp +++ b/tests/unit/query_trigger.cpp @@ -1,12 +1,22 @@ #include #include +#include #include "query/db_accessor.hpp" #include "query/interpreter.hpp" #include "query/trigger.hpp" #include "query/typed_value.hpp" #include "utils/memory.hpp" +namespace { +const std::unordered_set kAllEventTypes{ + query::TriggerEventType::ANY, query::TriggerEventType::VERTEX_CREATE, query::TriggerEventType::EDGE_CREATE, + query::TriggerEventType::CREATE, query::TriggerEventType::VERTEX_DELETE, query::TriggerEventType::EDGE_DELETE, + query::TriggerEventType::DELETE, query::TriggerEventType::VERTEX_UPDATE, query::TriggerEventType::EDGE_UPDATE, + query::TriggerEventType::UPDATE, +}; +} // namespace + class TriggerContextTest : public ::testing::Test { public: void SetUp() override { db.emplace(); } @@ -31,7 +41,7 @@ void CheckTypedValueSize(const query::TriggerContext &trigger_context, const que const size_t expected_size, query::DbAccessor &dba) { auto typed_values = trigger_context.GetTypedValue(tag, &dba); ASSERT_TRUE(typed_values.IsList()); - ASSERT_EQ(typed_values.ValueList().size(), expected_size); + ASSERT_EQ(expected_size, typed_values.ValueList().size()); }; void CheckLabelList(const query::TriggerContext &trigger_context, const query::TriggerIdentifierTag tag, @@ -61,7 +71,7 @@ void CheckLabelList(const query::TriggerContext &trigger_context, const query::T // that exist (unless its explicitly created for the deleted object) TEST_F(TriggerContextTest, ValidObjectsTest) { query::TriggerContext trigger_context; - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{kAllEventTypes}; size_t vertex_count = 0; size_t edge_count = 0; @@ -95,7 +105,7 @@ TEST_F(TriggerContextTest, ValidObjectsTest) { dba.AdvanceCommand(); trigger_context = std::move(trigger_context_collector).TransformToTriggerContext(); - trigger_context_collector = query::TriggerContextCollector{}; + trigger_context_collector = query::TriggerContextCollector{kAllEventTypes}; // Should have all the created objects CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::CREATED_VERTICES, vertex_count, dba); @@ -181,7 +191,7 @@ TEST_F(TriggerContextTest, ValidObjectsTest) { ASSERT_FALSE(dba.Commit().HasError()); trigger_context = std::move(trigger_context_collector).TransformToTriggerContext(); - trigger_context_collector = query::TriggerContextCollector{}; + trigger_context_collector = query::TriggerContextCollector{kAllEventTypes}; CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::SET_VERTEX_PROPERTIES, vertex_count, dba); CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::SET_EDGE_PROPERTIES, edge_count, dba); @@ -250,7 +260,7 @@ TEST_F(TriggerContextTest, ValidObjectsTest) { // Binding the trigger context to transaction will mean that creating and updating an object in the same transaction // will return only the CREATE event. TEST_F(TriggerContextTest, ReturnCreateOnlyEvent) { - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{kAllEventTypes}; query::DbAccessor dba{&StartTransaction()}; @@ -311,13 +321,14 @@ void EXPECT_PROP_EQ(const query::TypedValue &a, const query::TypedValue &b) { EX // transaction) everything inbetween should be ignored. TEST_F(TriggerContextTest, GlobalPropertyChange) { query::DbAccessor dba{&StartTransaction()}; + const std::unordered_set event_types{query::TriggerEventType::VERTEX_UPDATE}; auto v = dba.InsertVertex(); dba.AdvanceCommand(); { SPDLOG_DEBUG("SET -> SET"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterSetObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue("Value"), query::TypedValue("ValueNew")); trigger_context_collector.RegisterSetObjectProperty(v, dba.NameToProperty("PROPERTY"), @@ -339,7 +350,7 @@ TEST_F(TriggerContextTest, GlobalPropertyChange) { { SPDLOG_DEBUG("SET -> REMOVE"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterSetObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue("Value"), query::TypedValue("ValueNew")); trigger_context_collector.RegisterRemovedObjectProperty(v, dba.NameToProperty("PROPERTY"), @@ -360,7 +371,7 @@ TEST_F(TriggerContextTest, GlobalPropertyChange) { { SPDLOG_DEBUG("REMOVE -> SET"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterRemovedObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue("Value")); trigger_context_collector.RegisterSetObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue(), @@ -382,7 +393,7 @@ TEST_F(TriggerContextTest, GlobalPropertyChange) { { SPDLOG_DEBUG("REMOVE -> REMOVE"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterRemovedObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue("Value")); trigger_context_collector.RegisterRemovedObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue()); @@ -402,7 +413,7 @@ TEST_F(TriggerContextTest, GlobalPropertyChange) { { SPDLOG_DEBUG("SET -> SET (no change on transaction level)"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterSetObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue("Value"), query::TypedValue("ValueNew")); trigger_context_collector.RegisterSetObjectProperty(v, dba.NameToProperty("PROPERTY"), @@ -416,7 +427,7 @@ TEST_F(TriggerContextTest, GlobalPropertyChange) { { SPDLOG_DEBUG("SET -> REMOVE (no change on transaction level)"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterSetObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue(), query::TypedValue("ValueNew")); trigger_context_collector.RegisterRemovedObjectProperty(v, dba.NameToProperty("PROPERTY"), @@ -430,7 +441,7 @@ TEST_F(TriggerContextTest, GlobalPropertyChange) { { SPDLOG_DEBUG("REMOVE -> SET (no change on transaction level)"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterRemovedObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue("Value")); trigger_context_collector.RegisterSetObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue(), @@ -444,7 +455,7 @@ TEST_F(TriggerContextTest, GlobalPropertyChange) { { SPDLOG_DEBUG("REMOVE -> REMOVE (no change on transaction level)"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterRemovedObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue()); trigger_context_collector.RegisterRemovedObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue()); const auto trigger_context = std::move(trigger_context_collector).TransformToTriggerContext(); @@ -456,7 +467,7 @@ TEST_F(TriggerContextTest, GlobalPropertyChange) { { SPDLOG_DEBUG("SET -> REMOVE -> SET -> REMOVE -> SET"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterSetObjectProperty(v, dba.NameToProperty("PROPERTY"), query::TypedValue("Value0"), query::TypedValue("Value1")); trigger_context_collector.RegisterRemovedObjectProperty(v, dba.NameToProperty("PROPERTY"), @@ -486,6 +497,7 @@ TEST_F(TriggerContextTest, GlobalPropertyChange) { // Same as above, but for label changes TEST_F(TriggerContextTest, GlobalLabelChange) { query::DbAccessor dba{&StartTransaction()}; + const std::unordered_set event_types{query::TriggerEventType::VERTEX_UPDATE}; auto v = dba.InsertVertex(); dba.AdvanceCommand(); @@ -495,7 +507,7 @@ TEST_F(TriggerContextTest, GlobalLabelChange) { // so REMOVE -> REMOVE and SET -> SET doesn't make sense { SPDLOG_DEBUG("SET -> REMOVE"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterSetVertexLabel(v, label_id); trigger_context_collector.RegisterRemovedVertexLabel(v, label_id); const auto trigger_context = std::move(trigger_context_collector).TransformToTriggerContext(); @@ -507,7 +519,7 @@ TEST_F(TriggerContextTest, GlobalLabelChange) { { SPDLOG_DEBUG("REMOVE -> SET"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterRemovedVertexLabel(v, label_id); trigger_context_collector.RegisterSetVertexLabel(v, label_id); const auto trigger_context = std::move(trigger_context_collector).TransformToTriggerContext(); @@ -519,7 +531,7 @@ TEST_F(TriggerContextTest, GlobalLabelChange) { { SPDLOG_DEBUG("SET -> REMOVE -> SET -> REMOVE -> SET"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterSetVertexLabel(v, label_id); trigger_context_collector.RegisterRemovedVertexLabel(v, label_id); trigger_context_collector.RegisterSetVertexLabel(v, label_id); @@ -540,7 +552,7 @@ TEST_F(TriggerContextTest, GlobalLabelChange) { { SPDLOG_DEBUG("REMOVE -> SET -> REMOVE -> SET -> REMOVE"); - query::TriggerContextCollector trigger_context_collector; + query::TriggerContextCollector trigger_context_collector{event_types}; trigger_context_collector.RegisterRemovedVertexLabel(v, label_id); trigger_context_collector.RegisterSetVertexLabel(v, label_id); trigger_context_collector.RegisterRemovedVertexLabel(v, label_id); @@ -560,6 +572,231 @@ TEST_F(TriggerContextTest, GlobalLabelChange) { } } +namespace { +struct ShouldRegisterExpectation { + bool creation{false}; + bool deletion{false}; + bool update{false}; +}; + +template +void CheckRegisterInfo(const query::TriggerContextCollector &collector, const ShouldRegisterExpectation &expectation) { + EXPECT_EQ(expectation.creation, collector.ShouldRegisterCreatedObject()); + EXPECT_EQ(expectation.deletion, collector.ShouldRegisterDeletedObject()); + EXPECT_EQ(expectation.update, collector.ShouldRegisterObjectPropertyChange()); +} + +size_t BoolToSize(const bool value) { return value ? 1 : 0; } + +void CheckFilters(const std::unordered_set &event_types, + const ShouldRegisterExpectation &vertex_expectation, + const ShouldRegisterExpectation &edge_expectation, storage::Storage::Accessor *accessor) { + query::TriggerContextCollector collector{event_types}; + { + SCOPED_TRACE("Checking vertex"); + CheckRegisterInfo(collector, vertex_expectation); + } + { + SCOPED_TRACE("Checking edge"); + CheckRegisterInfo(collector, edge_expectation); + } + EXPECT_EQ(collector.ShouldRegisterVertexLabelChange(), vertex_expectation.update); + + query::DbAccessor dba{accessor}; + + auto vertex_to_delete = dba.InsertVertex(); + auto vertex_to_modify = dba.InsertVertex(); + + auto from_vertex = dba.InsertVertex(); + auto to_vertex = dba.InsertVertex(); + auto maybe_edge_to_delete = dba.InsertEdge(&from_vertex, &to_vertex, dba.NameToEdgeType("EDGE")); + auto maybe_edge_to_modify = dba.InsertEdge(&from_vertex, &to_vertex, dba.NameToEdgeType("EDGE")); + auto &edge_to_delete = maybe_edge_to_delete.GetValue(); + auto &edge_to_modify = maybe_edge_to_modify.GetValue(); + + dba.AdvanceCommand(); + + const auto created_vertex = dba.InsertVertex(); + const auto maybe_created_edge = dba.InsertEdge(&from_vertex, &to_vertex, dba.NameToEdgeType("EDGE")); + const auto created_edge = maybe_created_edge.GetValue(); + collector.RegisterCreatedObject(created_vertex); + collector.RegisterCreatedObject(created_edge); + collector.RegisterDeletedObject(dba.RemoveEdge(&edge_to_delete).GetValue().value()); + collector.RegisterDeletedObject(dba.RemoveVertex(&vertex_to_delete).GetValue().value()); + collector.RegisterSetObjectProperty(vertex_to_modify, dba.NameToProperty("UPDATE"), query::TypedValue{1}, + query::TypedValue{2}); + collector.RegisterRemovedObjectProperty(vertex_to_modify, dba.NameToProperty("REMOVE"), query::TypedValue{1}); + collector.RegisterSetObjectProperty(edge_to_modify, dba.NameToProperty("UPDATE"), query::TypedValue{1}, + query::TypedValue{2}); + collector.RegisterRemovedObjectProperty(edge_to_modify, dba.NameToProperty("REMOVE"), query::TypedValue{1}); + collector.RegisterSetVertexLabel(vertex_to_modify, dba.NameToLabel("SET")); + collector.RegisterRemovedVertexLabel(vertex_to_modify, dba.NameToLabel("REMOVE")); + dba.AdvanceCommand(); + + const auto trigger_context = std::move(collector).TransformToTriggerContext(); + const auto created_vertices = BoolToSize(vertex_expectation.creation); + { + SCOPED_TRACE("CREATED_VERTICES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::CREATED_VERTICES, created_vertices, dba); + } + const auto created_edges = BoolToSize(edge_expectation.creation); + { + SCOPED_TRACE("CREATED_EDGES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::CREATED_EDGES, created_edges, dba); + } + { + SCOPED_TRACE("CREATED_OBJECTS"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::CREATED_OBJECTS, created_vertices + created_edges, + dba); + } + const auto deleted_vertices = BoolToSize(vertex_expectation.deletion); + { + SCOPED_TRACE("DELETED_VERTICES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::DELETED_VERTICES, deleted_vertices, dba); + } + const auto deleted_edges = BoolToSize(edge_expectation.deletion); + { + SCOPED_TRACE("DELETED_EDGES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::DELETED_EDGES, deleted_edges, dba); + } + { + SCOPED_TRACE("DELETED_OBJECTS"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::DELETED_OBJECTS, deleted_vertices + deleted_edges, + dba); + } + { + SCOPED_TRACE("SET_VERTEX_PROPERTIES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::SET_VERTEX_PROPERTIES, + BoolToSize(vertex_expectation.update), dba); + } + { + SCOPED_TRACE("SET_EDGE_PROPERTIES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::SET_EDGE_PROPERTIES, + BoolToSize(edge_expectation.update), dba); + } + { + SCOPED_TRACE("REMOVED_VERTEX_PROPERTIES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_PROPERTIES, + BoolToSize(vertex_expectation.update), dba); + } + { + SCOPED_TRACE("REMOVED_EDGE_PROPERTIES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::REMOVED_EDGE_PROPERTIES, + BoolToSize(edge_expectation.update), dba); + } + const auto set_and_removed_vertex_props_and_labels = BoolToSize(vertex_expectation.update) * 4; + { + SCOPED_TRACE("UPDATED_VERTICES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::UPDATED_VERTICES, + set_and_removed_vertex_props_and_labels, dba); + } + const auto set_and_removed_edge_props = BoolToSize(edge_expectation.update) * 2; + { + SCOPED_TRACE("UPDATED_EDGES"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::UPDATED_EDGES, set_and_removed_edge_props, dba); + } + // sum of the previous + { + SCOPED_TRACE("UPDATED_OBJECTS"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::UPDATED_OBJECTS, + set_and_removed_vertex_props_and_labels + set_and_removed_edge_props, dba); + } + { + SCOPED_TRACE("SET_VERTEX_LABELS"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::SET_VERTEX_LABELS, + BoolToSize(vertex_expectation.update), dba); + } + { + SCOPED_TRACE("REMOVED_VERTEX_LABELS"); + CheckTypedValueSize(trigger_context, query::TriggerIdentifierTag::REMOVED_VERTEX_LABELS, + BoolToSize(vertex_expectation.update), dba); + } + + dba.Abort(); +} +} // namespace + +TEST_F(TriggerContextTest, Filtering) { + using TET = query::TriggerEventType; + // Check all event type individually + { + SCOPED_TRACE("TET::ANY"); + CheckFilters({TET::ANY}, ShouldRegisterExpectation{true, true, true}, ShouldRegisterExpectation{true, true, true}, + &StartTransaction()); + } + { + SCOPED_TRACE("TET::VERTEX_CREATE"); + CheckFilters({TET::VERTEX_CREATE}, ShouldRegisterExpectation{true, false, false}, + ShouldRegisterExpectation{false, false, false}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::EDGE_CREATE"); + CheckFilters({TET::EDGE_CREATE}, ShouldRegisterExpectation{false, false, false}, + ShouldRegisterExpectation{true, false, false}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::CREATE"); + CheckFilters({TET::CREATE}, ShouldRegisterExpectation{true, false, false}, + ShouldRegisterExpectation{true, false, false}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::VERTEX_DELETE"); + CheckFilters({TET::VERTEX_DELETE}, ShouldRegisterExpectation{true, true, false}, + ShouldRegisterExpectation{false, false, false}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::EDGE_DELETE"); + CheckFilters({TET::EDGE_DELETE}, ShouldRegisterExpectation{false, false, false}, + ShouldRegisterExpectation{true, true, false}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::DELETE"); + CheckFilters({TET::DELETE}, ShouldRegisterExpectation{true, true, false}, + ShouldRegisterExpectation{true, true, false}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::VERTEX_UPDATE"); + CheckFilters({TET::VERTEX_UPDATE}, ShouldRegisterExpectation{true, false, true}, + ShouldRegisterExpectation{false, false, false}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::EDGE_UPDATE"); + CheckFilters({TET::EDGE_UPDATE}, ShouldRegisterExpectation{false, false, false}, + ShouldRegisterExpectation{true, false, true}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::UPDATE"); + CheckFilters({TET::UPDATE}, ShouldRegisterExpectation{true, false, true}, + ShouldRegisterExpectation{true, false, true}, &StartTransaction()); + } + // Some combined versions + { + SCOPED_TRACE("TET::VERTEX_UPDATE, TET::EDGE_UPDATE"); + CheckFilters({TET::VERTEX_UPDATE, TET::EDGE_UPDATE}, ShouldRegisterExpectation{true, false, true}, + ShouldRegisterExpectation{true, false, true}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::VERTEX_UPDATE, TET::EDGE_UPDATE, TET::DELETE"); + CheckFilters({TET::VERTEX_UPDATE, TET::EDGE_UPDATE, TET::DELETE}, ShouldRegisterExpectation{true, true, true}, + ShouldRegisterExpectation{true, true, true}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::UPDATE, TET::VERTEX_DELETE, TET::EDGE_DELETE"); + CheckFilters({TET::UPDATE, TET::VERTEX_DELETE, TET::EDGE_DELETE}, ShouldRegisterExpectation{true, true, true}, + ShouldRegisterExpectation{true, true, true}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::VERTEX_CREATE, TET::VERTEX_UPDATE"); + CheckFilters({TET::VERTEX_CREATE, TET::VERTEX_UPDATE}, ShouldRegisterExpectation{true, false, true}, + ShouldRegisterExpectation{false, false, false}, &StartTransaction()); + } + { + SCOPED_TRACE("TET::EDGE_CREATE, TET::EDGE_UPDATE"); + CheckFilters({TET::EDGE_CREATE, TET::EDGE_UPDATE}, ShouldRegisterExpectation{false, false, false}, + ShouldRegisterExpectation{true, false, true}, &StartTransaction()); + } +} + class TriggerStoreTest : public ::testing::Test { protected: const std::filesystem::path testing_directory{std::filesystem::temp_directory_path() / "MG_test_unit_query_trigger"}; @@ -746,3 +983,24 @@ TEST_F(TriggerStoreTest, TriggerInfo) { check_trigger_info(); } + +TEST_F(TriggerStoreTest, AnyTriggerAllKeywords) { + query::TriggerStore store{testing_directory, &ast_cache, &*dba, &antlr_lock}; + + using namespace std::literals; + const std::array keywords = { + "createdVertices"sv, "createdEdges"sv, "createdObjects"sv, + "deletedVertices"sv, "deletedEdges"sv, "deletedObjects"sv, + "setVertexProperties"sv, "setEdgeProperties"sv, "removedVertexProperties"sv, + "removedEdgeProperties"sv, "setVertexLabels"sv, "removedVertexLabels"sv, + "updatedVertices"sv, "updatedEdges"sv, "updatedObjects"sv, + }; + + const auto trigger_name = "trigger"s; + + for (const auto keyword : keywords) { + ASSERT_NO_THROW(store.AddTrigger(trigger_name, fmt::format("RETURN {}", keyword), {}, query::TriggerEventType::ANY, + query::TriggerPhase::BEFORE_COMMIT, &ast_cache, &*dba, &antlr_lock)); + store.DropTrigger(trigger_name); + } +}