#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