diff --git a/include/_mgp.hpp b/include/_mgp.hpp index 2c3405ad2..85c0ebf4c 100644 --- a/include/_mgp.hpp +++ b/include/_mgp.hpp @@ -477,6 +477,8 @@ inline void path_destroy(mgp_path *path) { mgp_path_destroy(path); } inline void path_expand(mgp_path *path, mgp_edge *edge) { MgInvokeVoid(mgp_path_expand, path, edge); } +inline void path_pop(mgp_path *path) { MgInvokeVoid(mgp_path_pop, path); } + inline size_t path_size(mgp_path *path) { return MgInvoke(mgp_path_size, path); } inline mgp_vertex *path_vertex_at(mgp_path *path, size_t index) { diff --git a/include/_mgp_mock.py b/include/_mgp_mock.py index 5e940b3ee..308274faa 100644 --- a/include/_mgp_mock.py +++ b/include/_mgp_mock.py @@ -333,6 +333,13 @@ class Path: self._vertices.append(edge.end_id) self._edges.append((edge.start_id, edge.end_id, edge.id)) + def pop(self): + if not self._edges: + raise IndexError("Path contains no relationships.") + + self._vertices.pop() + self._edges.pop() + def vertex_at(self, index: int) -> Vertex: return Vertex(self._vertices[index], self._graph) diff --git a/include/mg_procedure.h b/include/mg_procedure.h index dd6107583..cf908ddce 100644 --- a/include/mg_procedure.h +++ b/include/mg_procedure.h @@ -543,6 +543,10 @@ void mgp_path_destroy(struct mgp_path *path); /// Return mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate memory for path extension. enum mgp_error mgp_path_expand(struct mgp_path *path, struct mgp_edge *edge); +/// Remove the last node and the last relationship from the path. +/// Return mgp_error::MGP_ERROR_OUT_OF_RANGE if the path contains no relationships. +enum mgp_error mgp_path_pop(struct mgp_path *path); + /// Get the number of edges in a mgp_path. /// Current implementation always returns without errors. enum mgp_error mgp_path_size(struct mgp_path *path, size_t *result); diff --git a/include/mgp.hpp b/include/mgp.hpp index 68bd0c75c..701b1e98d 100644 --- a/include/mgp.hpp +++ b/include/mgp.hpp @@ -886,6 +886,8 @@ class Path { /// @brief Adds a relationship continuing from the last node on the path. void Expand(const Relationship &relationship); + /// @brief Removes the last node and the last relationship from the path. + void Pop(); /// @exception std::runtime_error Path contains element(s) with unknown value. bool operator==(const Path &other) const; @@ -2995,6 +2997,8 @@ inline Relationship Path::GetRelationshipAt(size_t index) const { inline void Path::Expand(const Relationship &relationship) { mgp::path_expand(ptr_, relationship.ptr_); } +inline void Path::Pop() { mgp::path_pop(ptr_); } + inline bool Path::operator==(const Path &other) const { return util::PathsEqual(ptr_, other.ptr_); } inline bool Path::operator!=(const Path &other) const { return !(*this == other); } diff --git a/include/mgp.py b/include/mgp.py index 61e7aaa67..61a698815 100644 --- a/include/mgp.py +++ b/include/mgp.py @@ -983,6 +983,24 @@ class Path: self._vertices = None self._edges = None + def pop(self): + """ + Remove the last node and the last relationship from the path. + + Raises: + InvalidContextError: If using an invalid `Path` instance + OutOfRangeError: If the path contains no relationships. + + Examples: + ```path.pop()``` + """ + if not self.is_valid(): + raise InvalidContextError() + self._path.pop() + # Invalidate our cached tuples + self._vertices = None + self._edges = None + @property def vertices(self) -> typing.Tuple[Vertex, ...]: """ @@ -1023,6 +1041,10 @@ class Path: self._edges = tuple(Edge(self._path.edge_at(i)) for i in range(num_edges)) return self._edges + @property + def length(self) -> int: + return self._path.size() + class Record: """Represents a record of resulting field values.""" diff --git a/include/mgp_mock.py b/include/mgp_mock.py index 45be0edbd..1d84c855a 100644 --- a/include/mgp_mock.py +++ b/include/mgp_mock.py @@ -929,6 +929,25 @@ class Path: self._vertices = None self._edges = None + def pop(self): + """ + Remove the last node and the last relationship from the path. + + Raises: + InvalidContextError: If using an invalid `Path` instance + OutOfRangeError: If the path contains no relationships. + + Examples: + ```path.pop()``` + """ + if not self.is_valid(): + raise InvalidContextError() + self._path.pop() + + # Invalidate cached tuples + self._vertices = None + self._edges = None + @property def vertices(self) -> typing.Tuple[Vertex, ...]: """ diff --git a/src/query/procedure/mg_procedure_impl.cpp b/src/query/procedure/mg_procedure_impl.cpp index 7148faacf..0294b6f73 100644 --- a/src/query/procedure/mg_procedure_impl.cpp +++ b/src/query/procedure/mg_procedure_impl.cpp @@ -1180,6 +1180,17 @@ mgp_error mgp_path_expand(mgp_path *path, mgp_edge *edge) { }); } +mgp_error mgp_path_pop(struct mgp_path *path) { + return WrapExceptions([path] { + if (path->edges.empty()) { + throw std::out_of_range("Path contains no relationships."); + } + + path->vertices.pop_back(); + path->edges.pop_back(); + }); +} + namespace { size_t MgpPathSize(const mgp_path &path) noexcept { return path.edges.size(); } } // namespace diff --git a/src/query/procedure/py_module.cpp b/src/query/procedure/py_module.cpp index 37b0a6685..1e687a91e 100644 --- a/src/query/procedure/py_module.cpp +++ b/src/query/procedure/py_module.cpp @@ -2204,6 +2204,17 @@ PyObject *PyPathExpand(PyPath *self, PyObject *edge) { Py_RETURN_NONE; } +PyObject *PyPathPop(PyPath *self) { + MG_ASSERT(self->path); + MG_ASSERT(self->py_graph); + MG_ASSERT(self->py_graph->graph); + + if (RaiseExceptionFromErrorCode(mgp_path_pop(self->path))) { + return nullptr; + } + Py_RETURN_NONE; +} + PyObject *PyPathSize(PyPath *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->path); MG_ASSERT(self->py_graph); @@ -2251,6 +2262,8 @@ static PyMethodDef PyPathMethods[] = { "Create a path with a starting vertex."}, {"expand", reinterpret_cast(PyPathExpand), METH_O, "Append an edge continuing from the last vertex on the path."}, + {"pop", reinterpret_cast(PyPathPop), METH_NOARGS, + "Remove the last node and the last relationship from the path."}, {"size", reinterpret_cast(PyPathSize), METH_NOARGS, "Return the number of edges in a mgp_path."}, {"vertex_at", reinterpret_cast(PyPathVertexAt), METH_VARARGS, "Return the vertex from a path at given index."}, diff --git a/tests/e2e/mock_api/procedures/path.py b/tests/e2e/mock_api/procedures/path.py index 3e2499394..d560d94e5 100644 --- a/tests/e2e/mock_api/procedures/path.py +++ b/tests/e2e/mock_api/procedures/path.py @@ -33,6 +33,16 @@ def compare_apis(ctx: mgp.ProcCtx) -> mgp.Record(results_dict=mgp.Map): (2, 1), ) + path.pop() + mock_path.pop() + results["pop"] = test_utils.all_equal( + (len(path.vertices), len(path.edges)), + (len(mock_path.vertices), len(mock_path.edges)), + (1, 0), + ) + path.expand(edge_to_add) + mock_path.expand(mock_edge_to_add) + NEXT_ID = 1 results["vertices"] = test_utils.all_equal( all(isinstance(vertex, mgp.Vertex) for vertex in path.vertices), diff --git a/tests/e2e/mock_api/test_compare_mock.py b/tests/e2e/mock_api/test_compare_mock.py index 1f49913c8..e3b415229 100644 --- a/tests/e2e/mock_api/test_compare_mock.py +++ b/tests/e2e/mock_api/test_compare_mock.py @@ -125,6 +125,7 @@ def test_path(): "__copy__": True, "is_valid": True, "expand": True, + "pop": True, "vertices": True, "edges": True, } diff --git a/tests/unit/cpp_api.cpp b/tests/unit/cpp_api.cpp index be1270e9b..f425086f5 100644 --- a/tests/unit/cpp_api.cpp +++ b/tests/unit/cpp_api.cpp @@ -346,6 +346,9 @@ TYPED_TEST(CppApiTestFixture, TestPath) { auto value_x = mgp::Value(path); // Use Value move constructor auto value_y = mgp::Value(mgp::Path(node_0)); + + path.Pop(); + ASSERT_EQ(path.Length(), 0); } TYPED_TEST(CppApiTestFixture, TestDate) {