From 02eab6ab9ce0f3700f5218f8836cfd45ec12d496 Mon Sep 17 00:00:00 2001 From: Josipmrden <josip.mrden@memgraph.io> Date: Mon, 4 Sep 2023 16:17:43 +0200 Subject: [PATCH] Set properties C API extension (#1131) Add SetProperties into the C++ query module API --- include/_mgp.hpp | 8 ++ include/mg_procedure.h | 18 +++ include/mgp.hpp | 72 ++++++++-- include/mgp.py | 6 + src/query/db_accessor.hpp | 6 + src/query/procedure/mg_procedure_impl.cpp | 126 ++++++++++++++++++ src/query/procedure/py_module.cpp | 113 ++++++++++++++++ tests/e2e/CMakeLists.txt | 1 + tests/e2e/set_properties/CMakeLists.txt | 8 ++ tests/e2e/set_properties/common.py | 47 +++++++ .../set_properties/procedures/CMakeLists.txt | 1 + .../procedures/set_properties_module.py | 23 ++++ tests/e2e/set_properties/set_properties.py | 73 ++++++++++ tests/e2e/set_properties/workloads.yaml | 15 +++ tests/unit/cpp_api.cpp | 2 +- 15 files changed, 505 insertions(+), 14 deletions(-) create mode 100644 tests/e2e/set_properties/CMakeLists.txt create mode 100644 tests/e2e/set_properties/common.py create mode 100644 tests/e2e/set_properties/procedures/CMakeLists.txt create mode 100644 tests/e2e/set_properties/procedures/set_properties_module.py create mode 100644 tests/e2e/set_properties/set_properties.py create mode 100644 tests/e2e/set_properties/workloads.yaml diff --git a/include/_mgp.hpp b/include/_mgp.hpp index 22a0cf0be..17d4bc1e0 100644 --- a/include/_mgp.hpp +++ b/include/_mgp.hpp @@ -401,6 +401,10 @@ inline void vertex_set_property(mgp_vertex *v, const char *property_name, mgp_va MgInvokeVoid(mgp_vertex_set_property, v, property_name, property_value); } +inline void vertex_set_properties(mgp_vertex *v, struct mgp_map *properties) { + MgInvokeVoid(mgp_vertex_set_properties, v, properties); +} + inline mgp_properties_iterator *vertex_iter_properties(mgp_vertex *v, mgp_memory *memory) { return MgInvoke<mgp_properties_iterator *>(mgp_vertex_iter_properties, v, memory); } @@ -437,6 +441,10 @@ inline void edge_set_property(mgp_edge *e, const char *property_name, mgp_value MgInvokeVoid(mgp_edge_set_property, e, property_name, property_value); } +inline void edge_set_properties(mgp_edge *e, struct mgp_map *properties) { + MgInvokeVoid(mgp_edge_set_properties, e, properties); +} + inline mgp_properties_iterator *edge_iter_properties(mgp_edge *e, mgp_memory *memory) { return MgInvoke<mgp_properties_iterator *>(mgp_edge_iter_properties, e, memory); } diff --git a/include/mg_procedure.h b/include/mg_procedure.h index 78e80cc28..3ade1dfa6 100644 --- a/include/mg_procedure.h +++ b/include/mg_procedure.h @@ -664,6 +664,15 @@ enum mgp_error mgp_vertex_underlying_graph_is_mutable(struct mgp_vertex *v, int enum mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_name, struct mgp_value *property_value); +/// Set the value of properties on a vertex. +/// When the value is `null`, then the property is removed from the vertex. +/// Return mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate memory for storing the property. +/// Return mgp_error::MGP_ERROR_IMMUTABLE_OBJECT if `v` is immutable. +/// Return mgp_error::MGP_ERROR_DELETED_OBJECT if `v` has been deleted. +/// Return mgp_error::MGP_ERROR_SERIALIZATION_ERROR if `v` has been modified by another transaction. +/// Return mgp_error::MGP_ERROR_VALUE_CONVERSION if `property_value` is vertex, edge or path. +enum mgp_error mgp_vertex_set_properties(struct mgp_vertex *v, struct mgp_map *properties); + /// Add the label to the vertex. /// If the vertex already has the label, this function does nothing. /// Return mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate memory for storing the label. @@ -814,6 +823,15 @@ enum mgp_error mgp_edge_get_property(struct mgp_edge *e, const char *property_na /// Return mgp_error::MGP_ERROR_VALUE_CONVERSION if `property_value` is vertex, edge or path. enum mgp_error mgp_edge_set_property(struct mgp_edge *e, const char *property_name, struct mgp_value *property_value); +/// Set the value of properties on a vertex. +/// When the value is `null`, then the property is removed from the vertex. +/// Return mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate memory for storing the property. +/// Return mgp_error::MGP_ERROR_IMMUTABLE_OBJECT if `v` is immutable. +/// Return mgp_error::MGP_ERROR_DELETED_OBJECT if `v` has been deleted. +/// Return mgp_error::MGP_ERROR_SERIALIZATION_ERROR if `v` has been modified by another transaction. +/// Return mgp_error::MGP_ERROR_VALUE_CONVERSION if `property_value` is vertex, edge or path. +enum mgp_error mgp_edge_set_properties(struct mgp_edge *e, struct mgp_map *properties); + /// Start iterating over properties stored in the given edge. /// The properties of the edge are copied when the iterator is created, therefore later changes won't affect them. /// Resulting mgp_properties_iterator needs to be deallocated with diff --git a/include/mgp.hpp b/include/mgp.hpp index a56ac23cb..43436fd61 100644 --- a/include/mgp.hpp +++ b/include/mgp.hpp @@ -715,11 +715,14 @@ class Node { bool HasLabel(std::string_view label) const; /// @brief Returns an std::map of the node’s properties. - std::map<std::string, Value> Properties() const; + std::unordered_map<std::string, Value> Properties() const; /// @brief Sets the chosen property to the given value. void SetProperty(std::string property, Value value); + /// @brief Sets the chosen properties to the given values. + void SetProperties(std::unordered_map<std::string_view, Value> properties); + /// @brief Removes the chosen property. void RemoveProperty(std::string property); @@ -784,11 +787,14 @@ class Relationship { std::string_view Type() const; /// @brief Returns an std::map of the relationship’s properties. - std::map<std::string, Value> Properties() const; + std::unordered_map<std::string, Value> Properties() const; /// @brief Sets the chosen property to the given value. void SetProperty(std::string property, Value value); + /// @brief Sets the chosen properties to the given values. + void SetProperties(std::unordered_map<std::string_view, Value> properties); + /// @brief Removes the chosen property. void RemoveProperty(std::string property); @@ -2710,9 +2716,9 @@ inline void Node::RemoveLabel(const std::string_view label) { mgp::vertex_remove_label(this->ptr_, mgp_label{.name = label.data()}); } -inline std::map<std::string, Value> Node::Properties() const { +inline std::unordered_map<std::string, Value> Node::Properties() const { mgp_properties_iterator *properties_iterator = mgp::MemHandlerCallback(vertex_iter_properties, ptr_); - std::map<std::string, Value> property_map; + std::unordered_map<std::string, Value> property_map; for (auto *property = mgp::properties_iterator_get(properties_iterator); property; property = mgp::properties_iterator_next(properties_iterator)) { property_map.emplace(std::string(property->name), Value(property->value)); @@ -2725,6 +2731,17 @@ inline void Node::SetProperty(std::string property, Value value) { mgp::vertex_set_property(ptr_, property.data(), value.ptr()); } +inline void Node::SetProperties(std::unordered_map<std::string_view, Value> properties) { + mgp_map *map = mgp::MemHandlerCallback(map_make_empty); + + for (auto const &[k, v] : properties) { + mgp::map_insert(map, k.data(), v.ptr()); + } + + mgp::vertex_set_properties(ptr_, map); + mgp::map_destroy(map); +} + inline void Node::RemoveProperty(std::string property) { SetProperty(property, Value()); } inline Value Node::GetProperty(const std::string &property) const { @@ -2740,7 +2757,7 @@ inline bool Node::operator!=(const Node &other) const { return !(*this == other) // this functions is used both in relationship and node ToString inline std::string PropertiesToString(const std::map<std::string, Value> &property_map) { - std::string properties{""}; + std::string properties; const auto map_size = property_map.size(); size_t i = 0; for (const auto &[key, value] : property_map) { @@ -2762,8 +2779,14 @@ inline const std::string Node::ToString() const { if (labels == ", ") { labels = ""; // dont use labels if they dont exist } - std::map<std::string, Value> properties_map{Properties()}; - std::string properties{PropertiesToString(properties_map)}; + std::unordered_map<std::string, Value> properties_map{Properties()}; + std::map<std::string, Value> properties_map_sorted{}; + + for (const auto &[k, v] : properties_map) { + properties_map_sorted.emplace(k, v); + } + std::string properties{PropertiesToString(properties_map_sorted)}; + return "(id: " + std::to_string(Id().AsInt()) + labels + ", properties: {" + properties + "})"; } @@ -2807,9 +2830,9 @@ inline mgp::Id Relationship::Id() const { return Id::FromInt(mgp::edge_get_id(pt inline std::string_view Relationship::Type() const { return mgp::edge_get_type(ptr_).name; } -inline std::map<std::string, Value> Relationship::Properties() const { +inline std::unordered_map<std::string, Value> Relationship::Properties() const { mgp_properties_iterator *properties_iterator = mgp::MemHandlerCallback(edge_iter_properties, ptr_); - std::map<std::string, Value> property_map; + std::unordered_map<std::string, Value> property_map; for (mgp_property *property = mgp::properties_iterator_get(properties_iterator); property; property = mgp::properties_iterator_next(properties_iterator)) { property_map.emplace(property->name, Value(property->value)); @@ -2822,6 +2845,17 @@ inline void Relationship::SetProperty(std::string property, Value value) { mgp::edge_set_property(ptr_, property.data(), value.ptr()); } +inline void Relationship::SetProperties(std::unordered_map<std::string_view, Value> properties) { + mgp_map *map = mgp::MemHandlerCallback(map_make_empty); + + for (auto const &[k, v] : properties) { + mgp::map_insert(map, k.data(), v.ptr()); + } + + mgp::edge_set_properties(ptr_, map); + mgp::map_destroy(map); +} + inline void Relationship::RemoveProperty(std::string property) { SetProperty(property, Value()); } inline Value Relationship::GetProperty(const std::string &property) const { @@ -2846,8 +2880,14 @@ inline const std::string Relationship::ToString() const { const auto to = To(); const std::string type{Type()}; - std::map<std::string, Value> properties_map{Properties()}; - std::string properties{PropertiesToString(properties_map)}; + std::unordered_map<std::string, Value> properties_map{Properties()}; + std::map<std::string, Value> properties_map_sorted{}; + + for (const auto &[k, v] : properties_map) { + properties_map_sorted.emplace(k, v); + } + std::string properties{PropertiesToString(properties_map_sorted)}; + const std::string relationship{"[type: " + type + ", id: " + std::to_string(Id().AsInt()) + ", properties: {" + properties + "}]"}; @@ -2924,8 +2964,14 @@ inline const std::string Path::ToString() const { return_string.append(node.ToString() + "-"); const Relationship rel = GetRelationshipAt(i); - std::map<std::string, Value> properties_map{rel.Properties()}; - std::string properties = PropertiesToString(properties_map); + std::unordered_map<std::string, Value> properties_map{rel.Properties()}; + std::map<std::string, Value> properties_map_sorted{}; + + for (const auto &[k, v] : properties_map) { + properties_map_sorted.emplace(k, v); + } + std::string properties{PropertiesToString(properties_map_sorted)}; + return_string.append("[type: " + std::string(rel.Type()) + ", id: " + std::to_string(rel.Id().AsInt()) + ", properties: {" + properties + "}]->"); } diff --git a/include/mgp.py b/include/mgp.py index c1acea624..61e7aaa67 100644 --- a/include/mgp.py +++ b/include/mgp.py @@ -479,6 +479,12 @@ class Properties: except KeyError: return False + def set_properties(self, properties: dict) -> None: + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + self._vertex_or_edge.set_properties(properties) + class EdgeType: """Type of an Edge.""" diff --git a/src/query/db_accessor.hpp b/src/query/db_accessor.hpp index 120f9abac..cec29feed 100644 --- a/src/query/db_accessor.hpp +++ b/src/query/db_accessor.hpp @@ -245,6 +245,12 @@ class SubgraphVertexAccessor final { storage::Result<storage::PropertyValue> SetProperty(storage::PropertyId key, const storage::PropertyValue &value) { return impl_.SetProperty(key, value); } + + storage::Result<std::vector<std::tuple<storage::PropertyId, storage::PropertyValue, storage::PropertyValue>>> + UpdateProperties(std::map<storage::PropertyId, storage::PropertyValue> &properties) const { + return impl_.UpdateProperties(properties); + } + VertexAccessor GetVertexAccessor() const; }; } // namespace memgraph::query diff --git a/src/query/procedure/mg_procedure_impl.cpp b/src/query/procedure/mg_procedure_impl.cpp index dfe8f21cf..4c845eec5 100644 --- a/src/query/procedure/mg_procedure_impl.cpp +++ b/src/query/procedure/mg_procedure_impl.cpp @@ -1721,6 +1721,69 @@ mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_nam }); } +mgp_error mgp_vertex_set_properties(struct mgp_vertex *v, struct mgp_map *properties) { + return WrapExceptions([=] { + auto *ctx = v->graph->ctx; + +#ifdef MG_ENTERPRISE + if (memgraph::license::global_license_checker.IsEnterpriseValidFast() && ctx && ctx->auth_checker && + !ctx->auth_checker->Has(v->getImpl(), v->graph->view, + memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE)) { + throw AuthorizationException{"Insufficient permissions for setting properties on the vertex!"}; + } +#endif + if (!MgpVertexIsMutable(*v)) { + throw ImmutableObjectException{"Cannot set properties of an immutable vertex!"}; + } + + std::map<memgraph::storage::PropertyId, memgraph::storage::PropertyValue> props; + for (const auto &item : properties->items) { + props.insert(std::visit( + [&item](auto *impl) { + return std::make_pair(impl->NameToProperty(item.first), ToPropertyValue(item.second)); + }, + v->graph->impl)); + } + + const auto result = v->getImpl().UpdateProperties(props); + if (result.HasError()) { + switch (result.GetError()) { + case memgraph::storage::Error::DELETED_OBJECT: + throw DeletedObjectException{"Cannot set the properties of a deleted vertex!"}; + case memgraph::storage::Error::NONEXISTENT_OBJECT: + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when setting a property of a vertex!"); + case memgraph::storage::Error::PROPERTIES_DISABLED: + case memgraph::storage::Error::VERTEX_HAS_EDGES: + LOG_FATAL("Unexpected error when setting a property of a vertex."); + case memgraph::storage::Error::SERIALIZATION_ERROR: + throw SerializationException{"Cannot serialize setting a property of a vertex."}; + } + } + + ctx->execution_stats[memgraph::query::ExecutionStats::Key::UPDATED_PROPERTIES] += + static_cast<int64_t>(properties->items.size()); + + auto *trigger_ctx_collector = ctx->trigger_context_collector; + if (!trigger_ctx_collector || + !trigger_ctx_collector->ShouldRegisterObjectPropertyChange<memgraph::query::VertexAccessor>()) { + return; + } + + for (const auto &res : *result) { + const auto property_key = std::get<0>(res); + const auto old_value = memgraph::query::TypedValue(std::get<1>(res)); + const auto new_value = memgraph::query::TypedValue(std::get<2>(res)); + + if (new_value.IsNull()) { + trigger_ctx_collector->RegisterRemovedObjectProperty(v->getImpl(), property_key, old_value); + continue; + } + + trigger_ctx_collector->RegisterSetObjectProperty(v->getImpl(), property_key, old_value, new_value); + } + }); +} + mgp_error mgp_vertex_add_label(struct mgp_vertex *v, mgp_label label) { return WrapExceptions([=] { auto *ctx = v->graph->ctx; @@ -2288,6 +2351,69 @@ mgp_error mgp_edge_set_property(struct mgp_edge *e, const char *property_name, m }); } +mgp_error mgp_edge_set_properties(struct mgp_edge *e, struct mgp_map *properties) { + return WrapExceptions([=] { + auto *ctx = e->from.graph->ctx; + +#ifdef MG_ENTERPRISE + if (memgraph::license::global_license_checker.IsEnterpriseValidFast() && ctx && ctx->auth_checker && + !ctx->auth_checker->Has(e->impl, memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE)) { + throw AuthorizationException{"Insufficient permissions for setting properties on the edge!"}; + } +#endif + + if (!MgpEdgeIsMutable(*e)) { + throw ImmutableObjectException{"Cannot set properties of an immutable edge!"}; + } + std::map<memgraph::storage::PropertyId, memgraph::storage::PropertyValue> props; + for (const auto &item : properties->items) { + props.insert(std::visit( + [&item](auto *impl) { + return std::make_pair(impl->NameToProperty(item.first), ToPropertyValue(item.second)); + }, + e->from.graph->impl)); + } + + const auto result = e->impl.UpdateProperties(props); + if (result.HasError()) { + switch (result.GetError()) { + case memgraph::storage::Error::DELETED_OBJECT: + throw DeletedObjectException{"Cannot set the properties of a deleted edge!"}; + case memgraph::storage::Error::NONEXISTENT_OBJECT: + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when setting a property of an edge!"); + case memgraph::storage::Error::PROPERTIES_DISABLED: + throw std::logic_error{"Cannot set the properties of edges, because properties on edges are disabled!"}; + case memgraph::storage::Error::VERTEX_HAS_EDGES: + LOG_FATAL("Unexpected error when setting a property of an edge."); + case memgraph::storage::Error::SERIALIZATION_ERROR: + throw SerializationException{"Cannot serialize setting a property of an edge."}; + } + } + + ctx->execution_stats[memgraph::query::ExecutionStats::Key::UPDATED_PROPERTIES] += + static_cast<int64_t>(properties->items.size()); + + auto *trigger_ctx_collector = ctx->trigger_context_collector; + if (!trigger_ctx_collector || + !trigger_ctx_collector->ShouldRegisterObjectPropertyChange<memgraph::query::EdgeAccessor>()) { + return; + } + + for (const auto &res : *result) { + const auto property_key = std::get<0>(res); + const auto old_value = memgraph::query::TypedValue(std::get<1>(res)); + const auto new_value = memgraph::query::TypedValue(std::get<2>(res)); + + if (new_value.IsNull()) { + trigger_ctx_collector->RegisterRemovedObjectProperty(e->impl, property_key, old_value); + continue; + } + + trigger_ctx_collector->RegisterSetObjectProperty(e->impl, property_key, old_value, new_value); + } + }); +} + mgp_error mgp_edge_iter_properties(mgp_edge *e, mgp_memory *memory, mgp_properties_iterator **result) { // NOTE: This copies the whole properties into iterator. // TODO: Think of a good way to avoid the copy which doesn't just rely on some diff --git a/src/query/procedure/py_module.cpp b/src/query/procedure/py_module.cpp index 5e6fd8761..37b0a6685 100644 --- a/src/query/procedure/py_module.cpp +++ b/src/query/procedure/py_module.cpp @@ -1685,6 +1685,60 @@ PyObject *PyEdgeSetProperty(PyEdge *self, PyObject *args) { Py_RETURN_NONE; } +PyObject *PyEdgeSetProperties(PyEdge *self, PyObject *args) { + MG_ASSERT(self); + MG_ASSERT(self->edge); + MG_ASSERT(self->py_graph); + MG_ASSERT(self->py_graph->graph); + + PyObject *props{nullptr}; + if (!PyArg_ParseTuple(args, "O", &props)) { + return nullptr; + } + + MgpUniquePtr<mgp_map> properties_map{nullptr, mgp_map_destroy}; + const auto map_err = CreateMgpObject(properties_map, mgp_map_make_empty, self->py_graph->memory); + + if (map_err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) { + throw std::bad_alloc{}; + } + if (map_err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::runtime_error{"Unexpected error during creating mgp_map"}; + } + + PyObject *key{nullptr}; + PyObject *value{nullptr}; + Py_ssize_t pos{0}; + while (PyDict_Next(props, &pos, &key, &value)) { + // NOLINTNEXTLINE(hicpp-signed-bitwise) + if (!PyUnicode_Check(key)) { + throw std::invalid_argument("Dictionary keys must be strings"); + } + + const char *k = PyUnicode_AsUTF8(key); + + if (!k) { + PyErr_Clear(); + throw std::bad_alloc{}; + } + + MgpUniquePtr<mgp_value> prop_value{PyObjectToMgpValueWithPythonExceptions(value, self->py_graph->memory), + mgp_value_destroy}; + + if (const auto err = mgp_map_insert(properties_map.get(), k, prop_value.get()); + err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) { + throw std::bad_alloc{}; + } else if (err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::runtime_error{"Unexpected error during inserting an item to mgp_map"}; + } + } + + if (RaiseExceptionFromErrorCode(mgp_edge_set_properties(self->edge, properties_map.get()))) { + return nullptr; + } + + Py_RETURN_NONE; +} static PyMethodDef PyEdgeMethods[] = { {"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported."}, {"is_valid", reinterpret_cast<PyCFunction>(PyEdgeIsValid), METH_NOARGS, @@ -1701,6 +1755,8 @@ static PyMethodDef PyEdgeMethods[] = { "Return edge property with given name."}, {"set_property", reinterpret_cast<PyCFunction>(PyEdgeSetProperty), METH_VARARGS, "Set the value of the property on the edge."}, + {"set_properties", reinterpret_cast<PyCFunction>(PyEdgeSetProperties), METH_VARARGS, + "Set the values of the properties on the edge."}, {nullptr, {}, {}, {}}, }; @@ -1935,6 +1991,61 @@ PyObject *PyVertexSetProperty(PyVertex *self, PyObject *args) { Py_RETURN_NONE; } +PyObject *PyVertexSetProperties(PyVertex *self, PyObject *args) { + MG_ASSERT(self); + MG_ASSERT(self->vertex); + MG_ASSERT(self->py_graph); + MG_ASSERT(self->py_graph->graph); + + PyObject *props{nullptr}; + if (!PyArg_ParseTuple(args, "O", &props)) { + return nullptr; + } + + MgpUniquePtr<mgp_map> properties_map{nullptr, mgp_map_destroy}; + const auto map_err = CreateMgpObject(properties_map, mgp_map_make_empty, self->py_graph->memory); + + if (map_err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) { + throw std::bad_alloc{}; + } + if (map_err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::runtime_error{"Unexpected error during creating mgp_map"}; + } + + PyObject *key{nullptr}; + PyObject *value{nullptr}; + Py_ssize_t pos{0}; + while (PyDict_Next(props, &pos, &key, &value)) { + // NOLINTNEXTLINE(hicpp-signed-bitwise) + if (!PyUnicode_Check(key)) { + throw std::invalid_argument("Dictionary keys must be strings"); + } + + const char *k = PyUnicode_AsUTF8(key); + + if (!k) { + PyErr_Clear(); + throw std::bad_alloc{}; + } + + MgpUniquePtr<mgp_value> prop_value{PyObjectToMgpValueWithPythonExceptions(value, self->py_graph->memory), + mgp_value_destroy}; + + if (const auto err = mgp_map_insert(properties_map.get(), k, prop_value.get()); + err == mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE) { + throw std::bad_alloc{}; + } else if (err != mgp_error::MGP_ERROR_NO_ERROR) { + throw std::runtime_error{"Unexpected error during inserting an item to mgp_map"}; + } + } + + if (RaiseExceptionFromErrorCode(mgp_vertex_set_properties(self->vertex, properties_map.get()))) { + return nullptr; + } + + Py_RETURN_NONE; +} + PyObject *PyVertexAddLabel(PyVertex *self, PyObject *args) { MG_ASSERT(self); MG_ASSERT(self->vertex); @@ -1989,6 +2100,8 @@ static PyMethodDef PyVertexMethods[] = { "Return vertex property with given name."}, {"set_property", reinterpret_cast<PyCFunction>(PyVertexSetProperty), METH_VARARGS, "Set the value of the property on the vertex."}, + {"set_properties", reinterpret_cast<PyCFunction>(PyVertexSetProperties), METH_VARARGS, + "Set the values of the properties on the vertex."}, {nullptr, {}, {}, {}}, }; diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index b839dedc5..a9abdeb10 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -62,6 +62,7 @@ add_subdirectory(init_file_flags) add_subdirectory(analytical_mode) add_subdirectory(batched_procedures) add_subdirectory(concurrent_query_modules) +add_subdirectory(set_properties) copy_e2e_python_files(pytest_runner pytest_runner.sh "") file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.crt DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/tests/e2e/set_properties/CMakeLists.txt b/tests/e2e/set_properties/CMakeLists.txt new file mode 100644 index 000000000..10cc03584 --- /dev/null +++ b/tests/e2e/set_properties/CMakeLists.txt @@ -0,0 +1,8 @@ +function(copy_set_properties_e2e_python_files FILE_NAME) + copy_e2e_python_files(set_properties ${FILE_NAME}) +endfunction() + +copy_set_properties_e2e_python_files(common.py) +copy_set_properties_e2e_python_files(set_properties.py) + +add_subdirectory(procedures) diff --git a/tests/e2e/set_properties/common.py b/tests/e2e/set_properties/common.py new file mode 100644 index 000000000..80d00cfbd --- /dev/null +++ b/tests/e2e/set_properties/common.py @@ -0,0 +1,47 @@ +# Copyright 2023 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import typing + +import mgclient +import pytest +from gqlalchemy import Memgraph + + +def execute_and_fetch_all(cursor: mgclient.Cursor, query: str, params: dict = {}) -> typing.List[tuple]: + cursor.execute(query, params) + return cursor.fetchall() + + +@pytest.fixture +def connect(**kwargs) -> mgclient.Connection: + connection = mgclient.connect(host="localhost", port=7687, **kwargs) + connection.autocommit = True + cursor = connection.cursor() + execute_and_fetch_all(cursor, "USE DATABASE memgraph") + try: + execute_and_fetch_all(cursor, "DROP DATABASE clean") + except: + pass + execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n") + yield connection + + +@pytest.fixture +def memgraph(**kwargs) -> Memgraph: + memgraph = Memgraph() + + yield memgraph + + memgraph.drop_database() + memgraph.execute("analyze graph delete statistics;") + memgraph.drop_indexes() + memgraph.drop_triggers() diff --git a/tests/e2e/set_properties/procedures/CMakeLists.txt b/tests/e2e/set_properties/procedures/CMakeLists.txt new file mode 100644 index 000000000..e7186eb99 --- /dev/null +++ b/tests/e2e/set_properties/procedures/CMakeLists.txt @@ -0,0 +1 @@ +copy_set_properties_e2e_python_files(set_properties_module.py) diff --git a/tests/e2e/set_properties/procedures/set_properties_module.py b/tests/e2e/set_properties/procedures/set_properties_module.py new file mode 100644 index 000000000..aa2b1ebd3 --- /dev/null +++ b/tests/e2e/set_properties/procedures/set_properties_module.py @@ -0,0 +1,23 @@ +# Copyright 2021 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import mgp + + +@mgp.write_proc +def set_multiple_properties(ctx: mgp.ProcCtx, vertex_or_edge: mgp.Any) -> mgp.Record(updated=bool): + props = dict() + props["prop1"] = 1 + props["prop2"] = 2 + props["prop3"] = 3 + + vertex_or_edge.properties.set_properties(props) + return mgp.Record(updated=True) diff --git a/tests/e2e/set_properties/set_properties.py b/tests/e2e/set_properties/set_properties.py new file mode 100644 index 000000000..27b86585d --- /dev/null +++ b/tests/e2e/set_properties/set_properties.py @@ -0,0 +1,73 @@ +# Copyright 2023 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import sys + +import pytest +from common import memgraph + + +def test_set_multiple_properties_on_vertex_via_query_module(memgraph): + memgraph.execute( + """ +CREATE TRIGGER trigger ON () UPDATE BEFORE COMMIT EXECUTE +UNWIND updatedVertices AS updatedVertex +SET updatedVertex.vertex.updated = true; +""" + ) + + memgraph.execute("CREATE (n)") + has_updated = list( + memgraph.execute_and_fetch( + "MATCH (n) CALL set_properties_module.set_multiple_properties(n) YIELD updated RETURN updated" + ) + ) + + assert len(has_updated) == 1 + assert has_updated[0]["updated"] == True + + updated_vertex = next(memgraph.execute_and_fetch("MATCH (n) RETURN n"))["n"] + + assert updated_vertex._properties["prop1"] == 1 + assert updated_vertex._properties["prop2"] == 2 + assert updated_vertex._properties["prop3"] == 3 + assert updated_vertex._properties["updated"] == True + + +def test_set_multiple_properties_on_edge_via_query_module(memgraph): + memgraph.execute( + """ +CREATE TRIGGER trigger ON --> UPDATE BEFORE COMMIT EXECUTE +UNWIND updatedEdges AS updatedEdge +SET updatedEdge.edge.updated = true; +""" + ) + + memgraph.execute("CREATE (n)-[r:TYPE]->(m)") + has_updated = list( + memgraph.execute_and_fetch( + "MATCH ()-[r]->() CALL set_properties_module.set_multiple_properties(r) YIELD updated RETURN updated" + ) + ) + + assert len(has_updated) == 1 + assert has_updated[0]["updated"] == True + + updated_edge = next(memgraph.execute_and_fetch("MATCH ()-[r]->() RETURN r"))["r"] + + assert updated_edge._properties["prop1"] == 1 + assert updated_edge._properties["prop2"] == 2 + assert updated_edge._properties["prop3"] == 3 + assert updated_edge._properties["updated"] == True + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/set_properties/workloads.yaml b/tests/e2e/set_properties/workloads.yaml new file mode 100644 index 000000000..d29715176 --- /dev/null +++ b/tests/e2e/set_properties/workloads.yaml @@ -0,0 +1,15 @@ +set_properties_cluster: &set_properties_cluster + cluster: + main: + args: ["--bolt-port", "7687", "--log-level=TRACE"] + log_file: "analyze_graph.log" + setup_queries: [] + validation_queries: [] + + +workloads: + - name: "Setting multiple properties" + binary: "tests/e2e/pytest_runner.sh" + proc: "tests/e2e/set_properties/procedures/" + args: ["set_properties/set_properties.py"] + <<: *set_properties_cluster diff --git a/tests/unit/cpp_api.cpp b/tests/unit/cpp_api.cpp index 19852478e..57411f723 100644 --- a/tests/unit/cpp_api.cpp +++ b/tests/unit/cpp_api.cpp @@ -466,7 +466,7 @@ TYPED_TEST(CppApiTestFixture, TestNodeProperties) { ASSERT_EQ(node_1.Properties().size(), 0); - std::map<std::string, mgp::Value> node1_prop = node_1.Properties(); + std::unordered_map<std::string, mgp::Value> node1_prop = node_1.Properties(); node_1.SetProperty("b", mgp::Value("b")); ASSERT_EQ(node_1.Properties().size(), 1);