From be9ed7e879f97003118e60c8a9dda0d6f6b9c910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1nos=20Benjamin=20Antal?= Date: Wed, 22 Sep 2021 08:49:29 +0200 Subject: [PATCH] Python wrapper for write procedures (#234) * Rename mgp_graph_remove to mgp_graph_delete * Add mgp_graph_detach_delete * Add PyGraph functions * Add _mgp exceptions * Use unified error handling in python wrapper * Ignore clang-tidy warnings * Add mgp.Graph, mgp.Vertex and mgp.Edge mutable functions * Add python write procedure registration * Add `is_write` result field to mg.procedures * Use storage::View::NEW for write procedures * Add simple tests for write procedures * Remove false information about IDs --- include/mg_procedure.h | 57 +- include/mgp.py | 702 +++++++++++++--- src/query/plan/operator.cpp | 9 +- src/query/procedure/mg_procedure_impl.cpp | 61 +- src/query/procedure/module.cpp | 23 +- src/query/procedure/py_module.cpp | 793 ++++++++++++------ tests/e2e/CMakeLists.txt | 11 + ...treams_test_runner.sh => pytest_runner.sh} | 0 tests/e2e/streams/CMakeLists.txt | 8 +- tests/e2e/streams/workloads.yaml | 8 +- tests/e2e/write_procedures/CMakeLists.txt | 9 + tests/e2e/write_procedures/common.py | 23 + tests/e2e/write_procedures/conftest.py | 11 + .../procedures/CMakeLists.txt | 2 + tests/e2e/write_procedures/procedures/read.py | 12 + .../e2e/write_procedures/procedures/write.py | 74 ++ tests/e2e/write_procedures/simple_write.py | 182 ++++ tests/e2e/write_procedures/workloads.yaml | 14 + tests/unit/cypher_main_visitor.cpp | 6 +- tests/unit/query_procedures_mgp_graph.cpp | 35 +- 20 files changed, 1580 insertions(+), 460 deletions(-) rename tests/e2e/{streams/streams_test_runner.sh => pytest_runner.sh} (100%) create mode 100644 tests/e2e/write_procedures/CMakeLists.txt create mode 100644 tests/e2e/write_procedures/common.py create mode 100644 tests/e2e/write_procedures/conftest.py create mode 100644 tests/e2e/write_procedures/procedures/CMakeLists.txt create mode 100644 tests/e2e/write_procedures/procedures/read.py create mode 100644 tests/e2e/write_procedures/procedures/write.py create mode 100644 tests/e2e/write_procedures/simple_write.py create mode 100644 tests/e2e/write_procedures/workloads.yaml diff --git a/include/mg_procedure.h b/include/mg_procedure.h index 00bd5942d..84f16e35f 100644 --- a/include/mg_procedure.h +++ b/include/mg_procedure.h @@ -324,7 +324,6 @@ enum mgp_error mgp_list_append(struct mgp_list *list, struct mgp_value *val); /// original value. /// In case of a capacity change, the previously contained elements will move in /// memory and any references to them will be invalid. -/// Return MGP_ERROR_INSUFFICIENT_BUFFER if there's no capacity. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_value. enum mgp_error mgp_list_append_extend(struct mgp_list *list, struct mgp_value *val); @@ -527,13 +526,11 @@ struct mgp_vertex_id { }; /// Get the ID of given vertex. -/// The ID is only valid for a single query execution, you should never store it -/// globally in a query module. enum mgp_error mgp_vertex_get_id(struct mgp_vertex *v, struct mgp_vertex_id *result); /// Result is non-zero if the vertex can be modified. /// The mutability of the vertex is the same as the graph which it is part of. If a vertex is immutable, then edges -/// cannot be added or removed, properties and labels cannot be set or removed and all of the returned edges will be +/// cannot be created or deleted, properties and labels cannot be set or removed and all of the returned edges will be /// immutable also. /// Current implementation always returns without errors. enum mgp_error mgp_vertex_underlying_graph_is_mutable(struct mgp_vertex *v, int *result); @@ -542,7 +539,7 @@ enum mgp_error mgp_vertex_underlying_graph_is_mutable(struct mgp_vertex *v, int /// When the value is `null`, then the property is removed from the vertex. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate memory for storing the property. /// Return MGP_ERROR_IMMUTABLE_OBJECT if `v` is immutable. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. /// Return MGP_ERROR_SERIALIZATION_ERROR if `v` has been modified by another transaction. /// Return MGP_ERROR_VALUE_CONVERSION if `property_value` is vertex, edge or path. enum mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_name, @@ -552,14 +549,14 @@ enum mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *propert /// If the vertex already has the label, this function does nothing. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate memory for storing the label. /// Return MGP_ERROR_IMMUTABLE_OBJECT if `v` is immutable. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. /// Return MGP_ERROR_SERIALIZATION_ERROR if `v` has been modified by another transaction. enum mgp_error mgp_vertex_add_label(struct mgp_vertex *v, struct mgp_label label); /// Remove the label from the vertex. /// If the vertex doesn't have the label, this function does nothing. /// Return MGP_ERROR_IMMUTABLE_OBJECT if `v` is immutable. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. /// Return MGP_ERROR_SERIALIZATION_ERROR if `v` has been modified by another transaction. enum mgp_error mgp_vertex_remove_label(struct mgp_vertex *v, struct mgp_label label); @@ -575,26 +572,26 @@ void mgp_vertex_destroy(struct mgp_vertex *v); enum mgp_error mgp_vertex_equal(struct mgp_vertex *v1, struct mgp_vertex *v2, int *result); /// Get the number of labels a given vertex has. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_labels_count(struct mgp_vertex *v, size_t *result); /// Get mgp_label in mgp_vertex at given index. /// Return MGP_ERROR_OUT_OF_RANGE if the index is out of range. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_label_at(struct mgp_vertex *v, size_t index, struct mgp_label *result); /// Result is non-zero if the given vertex has the given label. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_has_label(struct mgp_vertex *v, struct mgp_label label, int *result); /// Result is non-zero if the given vertex has a label with given name. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_has_label_named(struct mgp_vertex *v, const char *label_name, int *result); /// Get a copy of a vertex property mapped to a given name. /// Resulting value must be freed with mgp_value_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_value. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_get_property(struct mgp_vertex *v, const char *property_name, struct mgp_memory *memory, struct mgp_value **result); @@ -602,7 +599,7 @@ enum mgp_error mgp_vertex_get_property(struct mgp_vertex *v, const char *propert /// The resulting mgp_properties_iterator needs to be deallocated with /// mgp_properties_iterator_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_properties_iterator. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_iter_properties(struct mgp_vertex *v, struct mgp_memory *memory, struct mgp_properties_iterator **result); @@ -610,7 +607,7 @@ enum mgp_error mgp_vertex_iter_properties(struct mgp_vertex *v, struct mgp_memor /// The resulting mgp_edges_iterator needs to be deallocated with /// mgp_edges_iterator_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_edges_iterator. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_iter_in_edges(struct mgp_vertex *v, struct mgp_memory *memory, struct mgp_edges_iterator **result); @@ -618,7 +615,7 @@ enum mgp_error mgp_vertex_iter_in_edges(struct mgp_vertex *v, struct mgp_memory /// The resulting mgp_edges_iterator needs to be deallocated with /// mgp_edges_iterator_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_edges_iterator. -/// Return MGP_ERROR_DELETED_OBJECT if `v` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `v` has been deleted. enum mgp_error mgp_vertex_iter_out_edges(struct mgp_vertex *v, struct mgp_memory *memory, struct mgp_edges_iterator **result); @@ -646,8 +643,6 @@ struct mgp_edge_id { }; /// Get the ID of given edge. -/// The ID is only valid for a single query execution, you should never store it -/// globally in a query module. enum mgp_error mgp_edge_get_id(struct mgp_edge *e, struct mgp_edge_id *result); /// Result is non-zero if the edge can be modified. @@ -683,7 +678,7 @@ enum mgp_error mgp_edge_get_to(struct mgp_edge *e, struct mgp_vertex **result); /// Get a copy of a edge property mapped to a given name. /// Resulting value must be freed with mgp_value_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_value. -/// Return MGP_ERROR_DELETED_OBJECT if `e` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `e` has been deleted. enum mgp_error mgp_edge_get_property(struct mgp_edge *e, const char *property_name, struct mgp_memory *memory, struct mgp_value **result); @@ -691,7 +686,7 @@ enum mgp_error mgp_edge_get_property(struct mgp_edge *e, const char *property_na /// When the value is `null`, then the property is removed from the edge. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate memory for storing the property. /// Return MGP_ERROR_IMMUTABLE_OBJECT if `e` is immutable. -/// Return MGP_ERROR_DELETED_OBJECT if `e` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `e` has been deleted. /// Return MGP_ERROR_LOGIC_ERROR if properties on edges are disabled. /// Return MGP_ERROR_SERIALIZATION_ERROR if `e` has been modified by another transaction. /// Return MGP_ERROR_VALUE_CONVERSION if `property_value` is vertex, edge or path. @@ -701,7 +696,7 @@ enum mgp_error mgp_edge_set_property(struct mgp_edge *e, const char *property_na /// Resulting mgp_properties_iterator needs to be deallocated with /// mgp_properties_iterator_destroy. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_properties_iterator. -/// Return MGP_ERROR_DELETED_OBJECT if `e` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `e` has been deleted. enum mgp_error mgp_edge_iter_properties(struct mgp_edge *e, struct mgp_memory *memory, struct mgp_properties_iterator **result); @@ -710,13 +705,13 @@ struct mgp_graph; /// Get the vertex corresponding to given ID, or NULL if no such vertex exists. /// Resulting vertex must be freed using mgp_vertex_destroy. -/// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate the vertex or if ID is not valid. +/// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate the vertex. enum mgp_error mgp_graph_get_vertex_by_id(struct mgp_graph *g, struct mgp_vertex_id id, struct mgp_memory *memory, struct mgp_vertex **result); /// Result is non-zero if the graph can be modified. -/// If a graph is immutable, then vertices cannot be added or removed, and all of the returned vertices will be -/// immutable also. +/// If a graph is immutable, then vertices cannot be created or deleted, and all of the returned vertices will be +/// immutable also. The same applies for edges. /// Current implementation always returns without errors. enum mgp_error mgp_graph_is_mutable(struct mgp_graph *graph, int *result); @@ -725,27 +720,31 @@ enum mgp_error mgp_graph_is_mutable(struct mgp_graph *graph, int *result); /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_vertex. enum mgp_error mgp_graph_create_vertex(struct mgp_graph *graph, struct mgp_memory *memory, struct mgp_vertex **result); -/// Remove a vertex from the graph. +/// Delete a vertex from the graph. /// Return MGP_ERROR_IMMUTABLE_OBJECT if `graph` is immutable. /// Return MGP_ERROR_LOGIC_ERROR if `vertex` has edges. /// Return MGP_ERROR_SERIALIZATION_ERROR if `vertex` has been modified by another transaction. -enum mgp_error mgp_graph_remove_vertex(struct mgp_graph *graph, struct mgp_vertex *vertex); +enum mgp_error mgp_graph_delete_vertex(struct mgp_graph *graph, struct mgp_vertex *vertex); + +/// Delete a vertex and all of its edges from the graph. +/// Return MGP_ERROR_IMMUTABLE_OBJECT if `graph` is immutable. +/// Return MGP_ERROR_SERIALIZATION_ERROR if `vertex` has been modified by another transaction. +enum mgp_error mgp_graph_detach_delete_vertex(struct mgp_graph *graph, struct mgp_vertex *vertex); /// Add a new directed edge between the two vertices with the specified label. /// NULL is returned if the the edge creation fails for any reason. /// Return MGP_ERROR_IMMUTABLE_OBJECT if `graph` is immutable. /// Return MGP_ERROR_UNABLE_TO_ALLOCATE if unable to allocate a mgp_edge. -/// Return MGP_ERROR_DELETED_OBJECT if `from` or `to` has been removed. +/// Return MGP_ERROR_DELETED_OBJECT if `from` or `to` has been deleted. /// Return MGP_ERROR_SERIALIZATION_ERROR if `from` or `to` has been modified by another transaction. enum mgp_error mgp_graph_create_edge(struct mgp_graph *graph, struct mgp_vertex *from, struct mgp_vertex *to, struct mgp_edge_type type, struct mgp_memory *memory, struct mgp_edge **result); -/// Remove an edge from the graph. +/// Delete an edge from the graph. /// Return MGP_ERROR_IMMUTABLE_OBJECT if `graph` is immutable. -/// Return MGP_ERROR_LOGIC_ERROR if `vertex` has edges. /// Return MGP_ERROR_SERIALIZATION_ERROR if `edge`, its source or destination vertex has been modified by another /// transaction. -enum mgp_error mgp_graph_remove_edge(struct mgp_graph *graph, struct mgp_edge *edge); +enum mgp_error mgp_graph_delete_edge(struct mgp_graph *graph, struct mgp_edge *edge); /// Iterator over vertices. struct mgp_vertices_iterator; diff --git a/include/mgp.py b/include/mgp.py index 97a930949..6fc135770 100644 --- a/include/mgp.py +++ b/include/mgp.py @@ -1,6 +1,6 @@ -''' +""" This module provides the API for usage in custom openCypher procedures. -''' +""" # C API using `mgp_memory` is not exposed in Python, instead the usage of such # API is hidden behind Python API. Any function requiring an instance of @@ -14,25 +14,110 @@ This module provides the API for usage in custom openCypher procedures. # actual implementation. Functions have type annotations as supported by Python # 3.5, but variable type annotations are only available with Python 3.6+ +from __future__ import annotations + from collections import namedtuple -import functools +from functools import wraps import inspect import sys import typing + import _mgp class InvalidContextError(Exception): - '''Signals using a graph element instance outside of the registered procedure.''' + """ + Signals using a graph element instance outside of the registered procedure. + """ + pass + + +class UnknownError(_mgp.UnknownError): + """ + Signals unspecified failure. + """ + pass + + +class UnableToAllocateError(_mgp.UnableToAllocateError): + """ + Signals failed memory allocation. + """ + pass + + +class InsufficientBufferError(_mgp.InsufficientBufferError): + """ + Signals that some buffer is not big enough. + """ + pass + + +class OutOfRangeError(_mgp.OutOfRangeError): + """ + Signals that an index-like parameter has a value that is outside its + possible values. + """ + pass + + +class LogicErrorError(_mgp.LogicErrorError): + """ + Signals faulty logic within the program such as violating logical + preconditions or class invariants and may be preventable. + """ + pass + + +class DeletedObjectError(_mgp.DeletedObjectError): + """ + Signals accessing an already deleted object. + """ + pass + + +class InvalidArgumentError(_mgp.InvalidArgumentError): + """ + Signals that some of the arguments have invalid values. + """ + pass + + +class KeyAlreadyExistsError(_mgp.KeyAlreadyExistsError): + """ + Signals that a key already exists in a container-like object. + """ + pass + + +class ImmutableObjectError(_mgp.ImmutableObjectError): + """ + Signals modification of an immutable object. + """ + pass + + +class ValueConversionError(_mgp.ValueConversionError): + """ + Signals that the conversion failed between python and cypher values. + """ + pass + + +class SerializationError(_mgp.SerializationError): + """ + Signals serialization error caused by concurrent modifications from + different transactions. + """ pass class Label: - '''Label of a Vertex.''' + """Label of a Vertex.""" __slots__ = ('_name',) - def __init__(self, name): + def __init__(self, name: str): self._name = name @property @@ -54,7 +139,9 @@ Property = namedtuple('Property', ('name', 'value')) class Properties: - '''A collection of properties either on a Vertex or an Edge.''' + """ + A collection of properties either on a Vertex or an Edge. + """ __slots__ = ('_vertex_or_edge', '_len',) def __init__(self, vertex_or_edge): @@ -72,10 +159,13 @@ class Properties: return Properties(self._vertex_or_edge) def get(self, property_name: str, default=None) -> object: - '''Get the value of a property with the given name or return default. + """ + Get the value of a property with the given name or return default. Raise InvalidContextError. - ''' + Raise UnableToAllocateError if unable to allocate a mgp.Value. + Raise DeletedObjectError if the object has been deleted. + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() try: @@ -83,8 +173,29 @@ class Properties: except KeyError: return default + def set(self, property_name: str, value: object) -> None: + """ + Set the value of the property. When the value is `None`, then the + property is removed. + + Raise UnableToAllocateError if unable to allocate memory for storing + the property. + Raise ImmutableObjectError if the object is immutable. + Raise DeletedObjectError if the ojbect has been deleted. + Raise SerializationError if the object has been modified by another + transaction. + Raise ValueConversionError if `value` is vertex, edge or path. + """ + self[property_name] = value + def items(self) -> typing.Iterable[Property]: - '''Raise InvalidContextError.''' + """ + Iterate over the properties. + + Raise InvalidContextError. + Raise UnableToAllocateError if unable to allocate an iterator. + Raise DeletedObjectError if the object has been deleted. + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() properties_it = self._vertex_or_edge.iter_properties() @@ -96,27 +207,39 @@ class Properties: prop = properties_it.next() def keys(self) -> typing.Iterable[str]: - '''Iterate over property names. + """ + Iterate over property names. Raise InvalidContextError. - ''' + Raise UnableToAllocateError if unable to allocate an iterator. + Raise DeletedObjectError if the object has been deleted. + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() for item in self.items(): yield item.name def values(self) -> typing.Iterable[object]: - '''Iterate over property values. + """ + Iterate over property values. Raise InvalidContextError. - ''' + Raise UnableToAllocateError if unable to allocate an iterator. + Raise DeletedObjectError if the object has been deleted. + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() for item in self.items(): yield item.value def __len__(self) -> int: - '''Raise InvalidContextError.''' + """ + Get the number of properties. + + Raise InvalidContextError. + Raise UnableToAllocateError if unable to allocate an iterator. + Raise DeletedObjectError if the object has been deleted. + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() if self._len is None: @@ -124,19 +247,26 @@ class Properties: return self._len def __iter__(self) -> typing.Iterable[str]: - '''Iterate over property names. + """ + Iterate over property names. Raise InvalidContextError. - ''' + Raise UnableToAllocateError if unable to allocate an iterator. + Raise DeletedObjectError if the object has been deleted. + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() for item in self.items(): yield item.name def __getitem__(self, property_name: str) -> object: - '''Get the value of a property with the given name or raise KeyError. + """ + Get the value of a property with the given name or raise KeyError. - Raise InvalidContextError.''' + Raise InvalidContextError. + Raise UnableToAllocateError if unable to allocate a mgp.Value. + Raise DeletedObjectError if the object has been deleted. + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() prop = self._vertex_or_edge.get_property(property_name) @@ -144,7 +274,32 @@ class Properties: raise KeyError() return prop + def __setitem__(self, property_name: str, value: object) -> None: + """ + Set the value of the property. When the value is `None`, then the + property is removed. + + Raise UnableToAllocateError if unable to allocate memory for storing + the property. + Raise ImmutableObjectError if the object is immutable. + Raise DeletedObjectError if the ojbect has been deleted. + Raise SerializationError if the object has been modified by another + transaction. + Raise ValueConversionError if `value` is vertex, edge or path. + """ + if not self._vertex_or_edge.is_valid(): + raise InvalidContextError() + + self._vertex_or_edge.set_property(property_name, value) + def __contains__(self, property_name: str) -> bool: + """ + Check if there is a property with the given name. + + Raise InvalidContextError. + Raise UnableToAllocateError if unable to allocate a mgp.Value. + Raise DeletedObjectError if the object has been deleted. + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() try: @@ -155,7 +310,7 @@ class Properties: class EdgeType: - '''Type of an Edge.''' + """Type of an Edge.""" __slots__ = ('_name',) def __init__(self, name): @@ -180,12 +335,12 @@ else: class Edge: - '''Edge in the graph database. + """Edge in the graph database. Access to an Edge is only valid during a single execution of a procedure in a query. You should not globally store an instance of an Edge. Using an invalid Edge instance will raise InvalidContextError. - ''' + """ __slots__ = ('_edge',) def __init__(self, edge): @@ -202,46 +357,72 @@ class Edge: return Edge(self._edge) def is_valid(self) -> bool: - '''Return True if `self` is in valid context and may be used.''' + """Return True if `self` is in valid context and may be used.""" return self._edge.is_valid() + def underlying_graph_is_mutable(self) -> bool: + """Return True if the edge can be modified.""" + if not self.is_valid(): + raise InvalidContextError() + return self._edge.underlying_graph_is_mutable() + @property def id(self) -> EdgeId: - '''Raise InvalidContextError.''' + """ + Get the ID of the edge. + + Raise InvalidContextError. + """ if not self.is_valid(): raise InvalidContextError() return self._edge.get_id() @property def type(self) -> EdgeType: - '''Raise InvalidContextError.''' + """ + Get the type of the edge. + + Raise InvalidContextError. + """ if not self.is_valid(): raise InvalidContextError() return EdgeType(self._edge.get_type_name()) @property - def from_vertex(self): # -> Vertex: - '''Raise InvalidContextError.''' + def from_vertex(self) -> Vertex: + """ + Get the source vertex. + + Raise InvalidContextError. + """ if not self.is_valid(): raise InvalidContextError() return Vertex(self._edge.from_vertex()) @property - def to_vertex(self): # -> Vertex: - '''Raise InvalidContextError.''' + def to_vertex(self) -> Vertex: + """ + Get the destination vertex. + + Raise InvalidContextError. + """ if not self.is_valid(): raise InvalidContextError() return Vertex(self._edge.to_vertex()) @property def properties(self) -> Properties: - '''Raise InvalidContextError.''' + """ + Get the properties of the edge. + + Raise InvalidContextError. + """ if not self.is_valid(): raise InvalidContextError() return Properties(self._edge) def __eq__(self, other) -> bool: - '''Raise InvalidContextError.''' + """Raise InvalidContextError.""" if not self.is_valid(): raise InvalidContextError() if not isinstance(other, Edge): @@ -259,12 +440,12 @@ else: class Vertex: - '''Vertex in the graph database. + """Vertex in the graph database. Access to a Vertex is only valid during a single execution of a procedure in a query. You should not globally store an instance of a Vertex. Using an invalid Vertex instance will raise InvalidContextError. - ''' + """ __slots__ = ('_vertex',) def __init__(self, vertex): @@ -281,34 +462,91 @@ class Vertex: return Vertex(self._vertex) def is_valid(self) -> bool: - '''Return True if `self` is in valid context and may be used''' + """Return True if `self` is in valid context and may be used.""" return self._vertex.is_valid() + def underlying_graph_is_mutable(self) -> bool: + """Return True if the vertex can be modified.""" + if not self.is_valid(): + raise InvalidContextError() + return self._vertex.underlying_graph_is_mutable() + @property def id(self) -> VertexId: - '''Raise InvalidContextError.''' + """ + Get the ID of the vertex. + + Raise InvalidContextError. + """ if not self.is_valid(): raise InvalidContextError() return self._vertex.get_id() @property - def labels(self) -> typing.List[Label]: - '''Raise InvalidContextError.''' + def labels(self) -> typing.Tuple[Label]: + """ + Get the labels of the vertex. + + Raise InvalidContextError. + Raise OutOfRangeError if some of the labels are removed while + collecting the labels. + Raise DeletedObjectError if `self` has been deleted. + """ if not self.is_valid(): raise InvalidContextError() return tuple(Label(self._vertex.label_at(i)) for i in range(self._vertex.labels_count())) + def add_label(self, label: str) -> None: + """ + Add the label to the vertex. + + Raise InvalidContextError. + Raise UnableToAllocateError if unable to allocate memory for storing + the label. + Raise ImmutableObjectError if `self` is immutable. + Raise DeletedObjectError if `self` has been deleted. + Raise SerializationError if `self` has been modified by another + transaction. + """ + if not self.is_valid(): + raise InvalidContextError() + return self._vertex.add_label(label) + + def remove_label(self, label: str) -> None: + """ + Remove the label from the vertex. + + Raise InvalidContextError. + Raise ImmutableObjectError if `self` is immutable. + Raise DeletedObjectError if `self` has been deleted. + Raise SerializationError if `self` has been modified by another + transaction. + """ + if not self.is_valid(): + raise InvalidContextError() + return self._vertex.remove_label(label) + @property def properties(self) -> Properties: - '''Raise InvalidContextError.''' + """ + Get the properties of the vertex. + + Raise InvalidContextError. + """ if not self.is_valid(): raise InvalidContextError() return Properties(self._vertex) @property def in_edges(self) -> typing.Iterable[Edge]: - '''Raise InvalidContextError.''' + """ + Iterate over inbound edges of the vertex. + + Raise InvalidContextError. + Raise UnableToAllocateError if unable to allocate an iterator. + Raise DeletedObjectError if `self` has been deleted. + """ if not self.is_valid(): raise InvalidContextError() edges_it = self._vertex.iter_in_edges() @@ -321,7 +559,13 @@ class Vertex: @property def out_edges(self) -> typing.Iterable[Edge]: - '''Raise InvalidContextError.''' + """ + Iterate over outbound edges of the vertex. + + Raise InvalidContextError. + Raise UnableToAllocateError if unable to allocate an iterator. + Raise DeletedObjectError if `self` has been deleted. + """ if not self.is_valid(): raise InvalidContextError() edges_it = self._vertex.iter_out_edges() @@ -333,7 +577,7 @@ class Vertex: edge = edges_it.next() def __eq__(self, other) -> bool: - '''Raise InvalidContextError''' + """Raise InvalidContextError""" if not self.is_valid(): raise InvalidContextError() if not isinstance(other, Vertex): @@ -345,14 +589,15 @@ class Vertex: class Path: - '''Path containing Vertex and Edge instances.''' + """Path containing Vertex and Edge instances.""" __slots__ = ('_path', '_vertices', '_edges') def __init__(self, starting_vertex_or_path: typing.Union[_mgp.Path, Vertex]): - '''Initialize with a starting Vertex. + """Initialize with a starting Vertex. Raise InvalidContextError if passed in Vertex is invalid. - ''' + Raise UnableToAllocateError if cannot allocate a path. + """ # We cache calls to `vertices` and `edges`, so as to avoid needless # allocations at the C level. self._vertices = None @@ -395,16 +640,18 @@ class Path: return self._path.is_valid() def expand(self, edge: Edge): - '''Append an edge continuing from the last vertex on the path. + """Append an edge continuing from the last vertex on the path. The last vertex on the path will become the other endpoint of the given edge, as continued from the current last vertex. - Raise ValueError if the current last vertex in the path is not part of - the given edge. Raise InvalidContextError if using an invalid Path instance or if passed in edge is invalid. - ''' + Raise LogicErrorError if the current last vertex in the path is not + part of the given edge. + Raise UnableToAllocateError if unable to allocate memory for path + extension. + """ if not isinstance(edge, Edge): raise TypeError( "Expected '_mgp.Edge', got '{}'".format(type(edge))) @@ -417,9 +664,11 @@ class Path: @property def vertices(self) -> typing.Tuple[Vertex, ...]: - '''Vertices ordered from the start to the end of the path. + """ + Vertices ordered from the start to the end of the path. - Raise InvalidContextError if using an invalid Path instance.''' + Raise InvalidContextError if using an invalid Path instance. + """ if not self.is_valid(): raise InvalidContextError() if self._vertices is None: @@ -430,9 +679,11 @@ class Path: @property def edges(self) -> typing.Tuple[Edge, ...]: - '''Edges ordered from the start to the end of the path. + """ + Edges ordered from the start to the end of the path. - Raise InvalidContextError if using an invalid Path instance.''' + Raise InvalidContextError if using an invalid Path instance. + """ if not self.is_valid(): raise InvalidContextError() if self._edges is None: @@ -443,16 +694,16 @@ class Path: class Record: - '''Represents a record of resulting field values.''' + """Represents a record of resulting field values.""" __slots__ = ('fields',) def __init__(self, **kwargs): - '''Initialize with name=value fields in kwargs.''' + """Initialize with name=value fields in kwargs.""" self.fields = kwargs class Vertices: - '''Iterable over vertices in a graph.''' + """Iterable over vertices in a graph.""" __slots__ = ('_graph', '_len') def __init__(self, graph): @@ -469,11 +720,17 @@ class Vertices: return Vertices(self._graph) def is_valid(self) -> bool: - '''Return True if `self` is in valid context and may be used.''' + """Return True if `self` is in valid context and may be used.""" return self._graph.is_valid() def __iter__(self) -> typing.Iterable[Vertex]: - '''Raise InvalidContextError if context is invalid.''' + """ + Iterate over vertices. + + Raise InvalidContextError if context is invalid. + Raise UnableToAllocateError if unable to allocate an iterator or + a vertex. + """ if not self.is_valid(): raise InvalidContextError() vertices_it = self._graph.iter_vertices() @@ -485,6 +742,9 @@ class Vertices: vertex = vertices_it.next() def __contains__(self, vertex): + """ + Raise UnableToAllocateError if unable to allocate the vertex. + """ try: _ = self._graph.get_vertex_by_id(vertex.id) return True @@ -492,13 +752,20 @@ class Vertices: return False def __len__(self): + """ + Get the number of vertices. + + Raise InvalidContextError if context is invalid. + Raise UnableToAllocateError if unable to allocate an iterator or + a vertex. + """ if not self._len: self._len = sum(1 for _ in self) return self._len class Graph: - '''State of the graph database in current ProcCtx.''' + """State of the graph database in current ProcCtx.""" __slots__ = ('_graph',) def __init__(self, graph): @@ -514,11 +781,12 @@ class Graph: return Graph(self._graph) def is_valid(self) -> bool: - '''Return True if `self` is in valid context and may be used.''' + """Return True if `self` is in valid context and may be used.""" return self._graph.is_valid() def get_vertex_by_id(self, vertex_id: VertexId) -> Vertex: - '''Return the Vertex corresponding to given vertex_id from the graph. + """ + Return the Vertex corresponding to given vertex_id from the graph. Access to a Vertex is only valid during a single execution of a procedure in a query. You should not globally store the returned @@ -526,7 +794,7 @@ class Graph: Raise IndexError if unable to find the given vertex_id. Raise InvalidContextError if context is invalid. - ''' + """ if not self.is_valid(): raise InvalidContextError() vertex = self._graph.get_vertex_by_id(vertex_id) @@ -534,30 +802,106 @@ class Graph: @property def vertices(self) -> Vertices: - '''All vertices in the graph. + """ + All vertices in the graph. Access to a Vertex is only valid during a single execution of a procedure in a query. You should not globally store the returned Vertex instances. Raise InvalidContextError if context is invalid. - ''' + """ if not self.is_valid(): raise InvalidContextError() return Vertices(self._graph) + def is_mutable(self) -> bool: + """ + Return True if `self` represents a mutable graph, thus it can be + used to modify vertices and edges. + """ + if not self.is_valid(): + raise InvalidContextError() + return self._graph.is_mutable() + + def create_vertex(self) -> Vertex: + """ + Create a vertex. + + Raise ImmutableObjectError if `self` is immutable. + Raise UnableToAllocateError if unable to allocate a vertex. + """ + if not self.is_valid(): + raise InvalidContextError() + return Vertex(self._graph.create_vertex()) + + def delete_vertex(self, vertex: Vertex) -> None: + """ + Delete a vertex. + + Raise ImmutableObjectError if `self` is immutable. + Raise LogicErrorError if `vertex` has edges. + Raise SerializationError if `vertex` has been modified by + another transaction. + """ + if not self.is_valid(): + raise InvalidContextError() + self._graph.delete_vertex(vertex._vertex) + + def detach_delete_vertex(self, vertex: Vertex) -> None: + """ + Delete a vertex and all of its edges. + + Raise ImmutableObjectError if `self` is immutable. + Raise SerializationError if `vertex` has been modified by + another transaction. + """ + if not self.is_valid(): + raise InvalidContextError() + self._graph.detach_delete_vertex(vertex._vertex) + + def create_edge(self, from_vertex: Vertex, to_vertex: Vertex, + edge_type: EdgeType) -> None: + """ + Create an edge. + + Raise ImmutableObjectError if `self ` is immutable. + Raise UnableToAllocateError if unable to allocate an edge. + Raise DeletedObjectError if `from_vertex` or `to_vertex` has + been deleted. + Raise SerializationError if `from_vertex` or `to_vertex` has + been modified by another transaction. + """ + if not self.is_valid(): + raise InvalidContextError() + return Edge(self._graph.create_edge(from_vertex._vertex, + to_vertex._vertex, edge_type.name)) + + def delete_edge(self, edge: Edge) -> None: + """ + Delete an edge. + + + Raise ImmutableObjectError if `self` is immutable. + Raise SerializationError if `edge`, its source or destination + vertex has been modified by another transaction. + """ + if not self.is_valid(): + raise InvalidContextError() + self._graph.delete_edge(edge._edge) + class AbortError(Exception): - '''Signals that the procedure was asked to abort its execution.''' + """Signals that the procedure was asked to abort its execution.""" pass class ProcCtx: - '''Context of a procedure being executed. + """Context of a procedure being executed. Access to a ProcCtx is only valid during a single execution of a procedure in a query. You should not globally store a ProcCtx instance. - ''' + """ __slots__ = ('_graph',) def __init__(self, graph): @@ -571,7 +915,7 @@ class ProcCtx: @property def graph(self) -> Graph: - '''Raise InvalidContextError if context is invalid.''' + """Raise InvalidContextError if context is invalid.""" if not self.is_valid(): raise InvalidContextError() return self._graph @@ -600,14 +944,14 @@ Nullable = typing.Optional class UnsupportedTypingError(Exception): - '''Signals a typing annotation is not supported as a _mgp.CypherType.''' + """Signals a typing annotation is not supported as a _mgp.CypherType.""" def __init__(self, type_): super().__init__("Unsupported typing annotation '{}'".format(type_)) def _typing_to_cypher_type(type_): - '''Convert typing annotation to a _mgp.CypherType instance.''' + """Convert typing annotation to a _mgp.CypherType instance.""" simple_types = { typing.Any: _mgp.type_nullable(_mgp.type_any()), object: _mgp.type_nullable(_mgp.type_any()), @@ -715,7 +1059,7 @@ def _typing_to_cypher_type(type_): # Procedure registration class Deprecated: - '''Annotate a resulting Record's field as deprecated.''' + """Annotate a resulting Record's field as deprecated.""" __slots__ = ('field_type',) def __init__(self, type_): @@ -735,9 +1079,52 @@ def raise_if_does_not_meet_requirements(func: typing.Callable[..., Record]): raise NotImplementedError("Generator functions are not supported") +def _register_proc(func: typing.Callable[..., Record], + is_write: bool): + raise_if_does_not_meet_requirements(func) + register_func = ( + _mgp.Module.add_write_procedure if is_write + else _mgp.Module.add_read_procedure) + sig = inspect.signature(func) + params = tuple(sig.parameters.values()) + if params and params[0].annotation is ProcCtx: + @wraps(func) + def wrapper(graph, args): + return func(ProcCtx(graph), *args) + params = params[1:] + mgp_proc = register_func(_mgp._MODULE, wrapper) + else: + @wraps(func) + def wrapper(graph, args): + return func(*args) + mgp_proc = register_func(_mgp._MODULE, wrapper) + for param in params: + name = param.name + type_ = param.annotation + if type_ is param.empty: + type_ = object + cypher_type = _typing_to_cypher_type(type_) + if param.default is param.empty: + mgp_proc.add_arg(name, cypher_type) + else: + mgp_proc.add_opt_arg(name, cypher_type, param.default) + if sig.return_annotation is not sig.empty: + record = sig.return_annotation + if not isinstance(record, Record): + raise TypeError("Expected '{}' to return 'mgp.Record', got '{}'" + .format(func.__name__, type(record))) + for name, type_ in record.fields.items(): + if isinstance(type_, Deprecated): + cypher_type = _typing_to_cypher_type(type_.field_type) + mgp_proc.add_deprecated_result(name, cypher_type) + else: + mgp_proc.add_result(name, _typing_to_cypher_type(type_)) + return func + + def read_proc(func: typing.Callable[..., Record]): - ''' - Register `func` as a a read-only procedure of the current module. + """ + Register `func` as a read-only procedure of the current module. `read_proc` is meant to be used as a decorator function to register module procedures. The registered `func` needs to be a callable which optionally @@ -774,52 +1161,62 @@ def read_proc(func: typing.Callable[..., Record]): CALL example.procedure(1, 2) YIELD args, result; CALL example.procedure(1) YIELD args, result; Naturally, you may pass in different arguments or yield less fields. - ''' - raise_if_does_not_meet_requirements(func) - sig = inspect.signature(func) - params = tuple(sig.parameters.values()) - if params and params[0].annotation is ProcCtx: - @functools.wraps(func) - def wrapper(graph, args): - return func(ProcCtx(graph), *args) - params = params[1:] - mgp_proc = _mgp._MODULE.add_read_procedure(wrapper) - else: - @functools.wraps(func) - def wrapper(graph, args): - return func(*args) - mgp_proc = _mgp._MODULE.add_read_procedure(wrapper) - for param in params: - name = param.name - type_ = param.annotation - if type_ is param.empty: - type_ = object - cypher_type = _typing_to_cypher_type(type_) - if param.default is param.empty: - mgp_proc.add_arg(name, cypher_type) - else: - mgp_proc.add_opt_arg(name, cypher_type, param.default) - if sig.return_annotation is not sig.empty: - record = sig.return_annotation - if not isinstance(record, Record): - raise TypeError("Expected '{}' to return 'mgp.Record', got '{}'" - .format(func.__name__, type(record))) - for name, type_ in record.fields.items(): - if isinstance(type_, Deprecated): - cypher_type = _typing_to_cypher_type(type_.field_type) - mgp_proc.add_deprecated_result(name, cypher_type) - else: - mgp_proc.add_result(name, _typing_to_cypher_type(type_)) - return func + """ + return _register_proc(func, False) + + +def write_proc(func: typing.Callable[..., Record]): + """ + Register `func` as a writeable procedure of the current module. + + `write_proc` is meant to be used as a decorator function to register module + procedures. The registered `func` needs to be a callable which optionally + takes `ProcCtx` as the first argument. Other arguments of `func` will be + bound to values passed in the cypherQuery. The full signature of `func` + needs to be annotated with types. The return type must be + `Record(field_name=type, ...)` and the procedure must produce either a + complete Record or None. To mark a field as deprecated, use + `Record(field_name=Deprecated(type), ...)`. Multiple records can be + produced by returning an iterable of them. Registering generator functions + is currently not supported. + + Example usage. + + ``` + import mgp + + @mgp.write_proc + def procedure(context: mgp.ProcCtx, + required_arg: mgp.Nullable[mgp.Any], + optional_arg: mgp.Nullable[mgp.Any] = None + ) -> mgp.Record(result=str, args=list): + args = [required_arg, optional_arg] + # Multiple rows can be produced by returning an iterable of mgp.Record + return mgp.Record(args=args, result='Hello World!') + ``` + + The example procedure above returns 2 fields: `args` and `result`. + * `args` is a copy of arguments passed to the procedure. + * `result` is the result of this procedure, a "Hello World!" string. + Any errors can be reported by raising an Exception. + + The procedure can be invoked in openCypher using the following calls: + CALL example.procedure(1, 2) YIELD args, result; + CALL example.procedure(1) YIELD args, result; + Naturally, you may pass in different arguments or yield less fields. + """ + return _register_proc(func, True) class InvalidMessageError(Exception): - '''Signals using a message instance outside of the registered transformation.''' + """ + Signals using a message instance outside of the registered transformation. + """ pass class Message: - '''Represents a message from a stream.''' + """Represents a message from a stream.""" __slots__ = ('_message',) def __init__(self, message): @@ -835,7 +1232,7 @@ class Message: return Message(self._message) def is_valid(self) -> bool: - '''Return True if `self` is in valid context and may be used.''' + """Return True if `self` is in valid context and may be used.""" return self._message.is_valid() def payload(self) -> bytes: @@ -860,12 +1257,12 @@ class Message: class InvalidMessagesError(Exception): - '''Signals using a messages instance outside of the registered transformation.''' + """Signals using a messages instance outside of the registered transformation.""" pass class Messages: - '''Represents a list of messages from a stream.''' + """Represents a list of messages from a stream.""" __slots__ = ('_messages',) def __init__(self, messages): @@ -881,28 +1278,28 @@ class Messages: return Messages(self._messages) def is_valid(self) -> bool: - '''Return True if `self` is in valid context and may be used.''' + """Return True if `self` is in valid context and may be used.""" return self._messages.is_valid() def message_at(self, id: int) -> Message: - '''Raise InvalidMessagesError if context is invalid.''' + """Raise InvalidMessagesError if context is invalid.""" if not self.is_valid(): raise InvalidMessagesError() return Message(self._messages.message_at(id)) def total_messages(self) -> int: - '''Raise InvalidContextError if context is invalid.''' + """Raise InvalidContextError if context is invalid.""" if not self.is_valid(): raise InvalidMessagesError() return self._messages.total_messages() class TransCtx: - '''Context of a transformation being executed. + """Context of a transformation being executed. Access to a TransCtx is only valid during a single execution of a transformation. You should not globally store a TransCtx instance. - ''' + """ __slots__ = ('_graph') def __init__(self, graph): @@ -916,7 +1313,7 @@ class TransCtx: @property def graph(self) -> Graph: - '''Raise InvalidContextError if context is invalid.''' + """Raise InvalidContextError if context is invalid.""" if not self.is_valid(): raise InvalidContextError() return self._graph @@ -931,13 +1328,74 @@ def transformation(func: typing.Callable[..., Record]): raise NotImplementedError( "Valid signatures for transformations are (TransCtx, Messages) or (Messages)") if params[0].annotation is TransCtx: - @functools.wraps(func) + @wraps(func) def wrapper(graph, messages): return func(TransCtx(graph), messages) _mgp._MODULE.add_transformation(wrapper) else: - @functools.wraps(func) + @wraps(func) def wrapper(graph, messages): return func(messages) _mgp._MODULE.add_transformation(wrapper) return func + + +def wrap_exceptions(): + def wrap_function(func): + @wraps(func) + def wrapped_func(*args, **kwargs): + try: + return func(*args, **kwargs) + except _mgp.UnknownError as e: + raise UnknownError(e) + except _mgp.UnableToAllocateError as e: + raise UnableToAllocateError(e) + except _mgp.InsufficientBufferError as e: + raise InsufficientBufferError(e) + except _mgp.OutOfRangeError as e: + raise OutOfRangeError(e) + except _mgp.LogicErrorError as e: + raise LogicErrorError(e) + except _mgp.DeletedObjectError as e: + raise DeletedObjectError(e) + except _mgp.InvalidArgumentError as e: + raise InvalidArgumentError(e) + except _mgp.KeyAlreadyExistsError as e: + raise KeyAlreadyExistsError(e) + except _mgp.ImmutableObjectError as e: + raise ImmutableObjectError(e) + except _mgp.ValueConversionError as e: + raise ValueConversionError(e) + except _mgp.SerializationError as e: + raise SerializationError(e) + return wrapped_func + + def wrap_prop_func(func): + return None if func is None else wrap_function(func) + + def wrap_member_functions(cls: type): + for name, obj in inspect.getmembers(cls): + if inspect.isfunction(obj): + setattr(cls, name, wrap_function(obj)) + elif isinstance(obj, property): + setattr(cls, name, property( + wrap_prop_func(obj.fget), + wrap_prop_func(obj.fset), + wrap_prop_func(obj.fdel), + obj.__doc__)) + + def defined_in_this_module(obj: object): + return getattr(obj, "__module__", "") == __name__ + + module = sys.modules[__name__] + for name, obj in inspect.getmembers(module): + if not defined_in_this_module(obj): + continue + if inspect.isclass(obj): + wrap_member_functions(obj) + if inspect.isfunction(obj) and obj != wrap_exceptions \ + and not name.startswith("_"): + setattr(module, name, wrap_function(obj)) + + +wrap_exceptions() diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 89c8847ac..33f9b4c6e 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -3728,11 +3728,6 @@ class CallProcedureCursor : public Cursor { result_.signature = nullptr; result_.rows.clear(); result_.error_msg.reset(); - // TODO: When we add support for write and eager procedures, we will need - // to plan this operator with Accumulate and pass in storage::View::NEW. - auto graph_view = storage::View::OLD; - ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor, - graph_view); // It might be a good idea to resolve the procedure name once, at the // start. Unfortunately, this could deadlock if we tried to invoke a // procedure from a module (read lock) and reload a module (write lock) @@ -3746,6 +3741,10 @@ class CallProcedureCursor : public Cursor { throw QueryRuntimeException("There is no procedure named '{}'.", self_->procedure_name_); } const auto &[module, proc] = *maybe_found; + const auto graph_view = proc->is_write_procedure ? storage::View::NEW : storage::View::OLD; + ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor, + graph_view); + result_.signature = &proc->results; // Use evaluation memory, as invoking a procedure is akin to a simple // evaluation of an expression. diff --git a/src/query/procedure/mg_procedure_impl.cpp b/src/query/procedure/mg_procedure_impl.cpp index d09078a9f..7fe1c28aa 100644 --- a/src/query/procedure/mg_procedure_impl.cpp +++ b/src/query/procedure/mg_procedure_impl.cpp @@ -1122,7 +1122,7 @@ mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_nam case storage::Error::DELETED_OBJECT: throw DeletedObjectException{"Cannot set the properties of a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when setting a property of a vertex!"); + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when setting a property of a vertex!"); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: LOG_FATAL("Unexpected error when setting a property of a vertex."); @@ -1145,7 +1145,7 @@ mgp_error mgp_vertex_add_label(struct mgp_vertex *v, mgp_label label) { case storage::Error::DELETED_OBJECT: throw DeletedObjectException{"Cannot add a label to a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when adding a label to a vertex!"); + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when adding a label to a vertex!"); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: LOG_FATAL("Unexpected error when adding a label to a vertex."); @@ -1168,7 +1168,7 @@ mgp_error mgp_vertex_remove_label(struct mgp_vertex *v, mgp_label label) { case storage::Error::DELETED_OBJECT: throw DeletedObjectException{"Cannot remove a label from a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when removing a label from a vertex!"); + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when removing a label from a vertex!"); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: LOG_FATAL("Unexpected error when removing a label from a vertex."); @@ -1201,7 +1201,7 @@ mgp_error mgp_vertex_labels_count(mgp_vertex *v, size_t *result) { case storage::Error::DELETED_OBJECT: throw DeletedObjectException{"Cannot get the labels of a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when getting vertex labels!"); + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when getting vertex labels!"); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: case storage::Error::SERIALIZATION_ERROR: @@ -1223,7 +1223,7 @@ mgp_error mgp_vertex_label_at(mgp_vertex *v, size_t i, mgp_label *result) { case storage::Error::DELETED_OBJECT: throw DeletedObjectException{"Cannot get a label of a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when getting a label of a vertex!"); + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when getting a label of a vertex!"); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: case storage::Error::SERIALIZATION_ERROR: @@ -1256,7 +1256,8 @@ mgp_error mgp_vertex_has_label_named(mgp_vertex *v, const char *name, int *resul throw DeletedObjectException{"Cannot check the existence of a label on a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: LOG_FATAL( - "Query modules mustn't have access to nonexistent objects when checking the existence of a label on " + "Query modules shouldn't have access to nonexistent objects when checking the existence of a label " + "on " "a vertex!"); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: @@ -1284,7 +1285,7 @@ mgp_error mgp_vertex_get_property(mgp_vertex *v, const char *name, mgp_memory *m throw DeletedObjectException{"Cannot get a property of a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: LOG_FATAL( - "Query modules mustn't have access to nonexistent objects when getting a property of a vertex."); + "Query modules shouldn't have access to nonexistent objects when getting a property of a vertex."); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: case storage::Error::SERIALIZATION_ERROR: @@ -1310,7 +1311,8 @@ mgp_error mgp_vertex_iter_properties(mgp_vertex *v, mgp_memory *memory, mgp_prop throw DeletedObjectException{"Cannot get the properties of a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: LOG_FATAL( - "Query modules mustn't have access to nonexistent objects when getting the properties of a vertex."); + "Query modules shouldn't have access to nonexistent objects when getting the properties of a " + "vertex."); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: case storage::Error::SERIALIZATION_ERROR: @@ -1337,7 +1339,7 @@ mgp_error mgp_vertex_iter_in_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges_ throw DeletedObjectException{"Cannot get the inbound edges of a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: LOG_FATAL( - "Query modules mustn't have access to nonexistent objects when getting the inbound edges of a " + "Query modules shouldn't have access to nonexistent objects when getting the inbound edges of a " "vertex."); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: @@ -1369,7 +1371,7 @@ mgp_error mgp_vertex_iter_out_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges throw DeletedObjectException{"Cannot get the outbound edges of a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: LOG_FATAL( - "Query modules mustn't have access to nonexistent objects when getting the outbound edges of a " + "Query modules shouldn't have access to nonexistent objects when getting the outbound edges of a " "vertex."); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: @@ -1483,7 +1485,8 @@ mgp_error mgp_edge_get_property(mgp_edge *e, const char *name, mgp_memory *memor case storage::Error::DELETED_OBJECT: throw DeletedObjectException{"Cannot get a property of a deleted edge!"}; case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when getting a property of an edge."); + LOG_FATAL( + "Query modules shouldn't have access to nonexistent objects when getting a property of an edge."); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: case storage::Error::SERIALIZATION_ERROR: @@ -1508,7 +1511,7 @@ mgp_error mgp_edge_set_property(struct mgp_edge *e, const char *property_name, m case storage::Error::DELETED_OBJECT: throw DeletedObjectException{"Cannot set the properties of a deleted edge!"}; case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when setting a property of an edge!"); + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when setting a property of an edge!"); case storage::Error::PROPERTIES_DISABLED: throw std::logic_error{"Cannot set the properties of edges, because properties on edges are disabled!"}; case storage::Error::VERTEX_HAS_EDGES: @@ -1535,7 +1538,7 @@ mgp_error mgp_edge_iter_properties(mgp_edge *e, mgp_memory *memory, mgp_properti throw DeletedObjectException{"Cannot get the properties of a deleted edge!"}; case storage::Error::NONEXISTENT_OBJECT: LOG_FATAL( - "Query modules mustn't have access to nonexistent objects when getting the properties of an edge."); + "Query modules shouldn't have access to nonexistent objects when getting the properties of an edge."); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: case storage::Error::SERIALIZATION_ERROR: @@ -1577,7 +1580,7 @@ mgp_error mgp_graph_create_vertex(struct mgp_graph *graph, mgp_memory *memory, m result); } -mgp_error mgp_graph_remove_vertex(struct mgp_graph *graph, mgp_vertex *vertex) { +mgp_error mgp_graph_delete_vertex(struct mgp_graph *graph, mgp_vertex *vertex) { return WrapExceptions([=] { if (!MgpGraphIsMutable(*graph)) { throw ImmutableObjectException{"Cannot remove a vertex from an immutable graph!"}; @@ -1587,7 +1590,7 @@ mgp_error mgp_graph_remove_vertex(struct mgp_graph *graph, mgp_vertex *vertex) { if (result.HasError()) { switch (result.GetError()) { case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when removing a vertex!"); + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when removing a vertex!"); case storage::Error::DELETED_OBJECT: case storage::Error::PROPERTIES_DISABLED: LOG_FATAL("Unexpected error when removing a vertex."); @@ -1600,6 +1603,28 @@ mgp_error mgp_graph_remove_vertex(struct mgp_graph *graph, mgp_vertex *vertex) { }); } +mgp_error mgp_graph_detach_delete_vertex(struct mgp_graph *graph, mgp_vertex *vertex) { + return WrapExceptions([=] { + if (!MgpGraphIsMutable(*graph)) { + throw ImmutableObjectException{"Cannot remove a vertex from an immutable graph!"}; + } + const auto result = graph->impl->DetachRemoveVertex(&vertex->impl); + + if (result.HasError()) { + switch (result.GetError()) { + case storage::Error::NONEXISTENT_OBJECT: + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when removing a vertex!"); + case storage::Error::DELETED_OBJECT: + case storage::Error::PROPERTIES_DISABLED: + case storage::Error::VERTEX_HAS_EDGES: + LOG_FATAL("Unexpected error when removing a vertex."); + case storage::Error::SERIALIZATION_ERROR: + throw SerializationException{"Cannot serialize removing a vertex."}; + } + } + }); +} + mgp_error mgp_graph_create_edge(mgp_graph *graph, mgp_vertex *from, mgp_vertex *to, mgp_edge_type type, mgp_memory *memory, mgp_edge **result) { return WrapExceptions( @@ -1614,7 +1639,7 @@ mgp_error mgp_graph_create_edge(mgp_graph *graph, mgp_vertex *from, mgp_vertex * case storage::Error::DELETED_OBJECT: throw DeletedObjectException{"Cannot add an edge to a deleted vertex!"}; case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when creating an edge!"); + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when creating an edge!"); case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: LOG_FATAL("Unexpected error when creating an edge."); @@ -1627,7 +1652,7 @@ mgp_error mgp_graph_create_edge(mgp_graph *graph, mgp_vertex *from, mgp_vertex * result); } -mgp_error mgp_graph_remove_edge(struct mgp_graph *graph, mgp_edge *edge) { +mgp_error mgp_graph_delete_edge(struct mgp_graph *graph, mgp_edge *edge) { return WrapExceptions([=] { if (!MgpGraphIsMutable(*graph)) { throw ImmutableObjectException{"Cannot remove an edge from an immutable graph!"}; @@ -1637,7 +1662,7 @@ mgp_error mgp_graph_remove_edge(struct mgp_graph *graph, mgp_edge *edge) { if (result.HasError()) { switch (result.GetError()) { case storage::Error::NONEXISTENT_OBJECT: - LOG_FATAL("Query modules mustn't have access to nonexistent objects when removing an edge!"); + LOG_FATAL("Query modules shouldn't have access to nonexistent objects when removing an edge!"); case storage::Error::DELETED_OBJECT: case storage::Error::PROPERTIES_DISABLED: case storage::Error::VERTEX_HAS_EDGES: diff --git a/src/query/procedure/module.cpp b/src/query/procedure/module.cpp index a9777ceee..fbcb93238 100644 --- a/src/query/procedure/module.cpp +++ b/src/query/procedure/module.cpp @@ -162,7 +162,18 @@ void RegisterMgProcedures( PrintProcSignature(proc, &ss); const auto signature = ss.str(); MgpUniquePtr signature_value{nullptr, mgp_value_destroy}; - if (const auto err = CreateMgpObject(signature_value, mgp_value_make_string, full_name.c_str(), memory); + if (const auto err = CreateMgpObject(signature_value, mgp_value_make_string, signature.c_str(), memory); + err == MGP_ERROR_UNABLE_TO_ALLOCATE) { + static_cast(mgp_result_set_error_msg(result, "Not enough memory!")); + return; + } else if (err != MGP_ERROR_NO_ERROR) { + static_cast(mgp_result_set_error_msg(result, "Unexpected error")); + return; + } + MgpUniquePtr is_write_value{nullptr, mgp_value_destroy}; + + if (const auto err = + CreateMgpObject(is_write_value, mgp_value_make_bool, proc.is_write_procedure ? 1 : 0, memory); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { static_cast(mgp_result_set_error_msg(result, "Not enough memory!")); return; @@ -172,7 +183,8 @@ void RegisterMgProcedures( } const auto err1 = mgp_result_record_insert(record, "name", name_value.get()); const auto err2 = mgp_result_record_insert(record, "signature", signature_value.get()); - if (err1 != MGP_ERROR_NO_ERROR || err2 != MGP_ERROR_NO_ERROR) { + const auto err3 = mgp_result_record_insert(record, "is_write", is_write_value.get()); + if (err1 != MGP_ERROR_NO_ERROR || err2 != MGP_ERROR_NO_ERROR || err3 != MGP_ERROR_NO_ERROR) { static_cast(mgp_result_set_error_msg(result, "Unable to set the result!")); return; } @@ -182,13 +194,14 @@ void RegisterMgProcedures( mgp_proc procedures("procedures", procedures_cb, utils::NewDeleteResource(), false); MG_ASSERT(mgp_proc_add_result(&procedures, "name", Call(mgp_type_string)) == MGP_ERROR_NO_ERROR); MG_ASSERT(mgp_proc_add_result(&procedures, "signature", Call(mgp_type_string)) == MGP_ERROR_NO_ERROR); + MG_ASSERT(mgp_proc_add_result(&procedures, "is_write", Call(mgp_type_bool)) == MGP_ERROR_NO_ERROR); module->AddProcedure("procedures", std::move(procedures)); } void RegisterMgTransformations(const std::map, std::less<>> *all_modules, BuiltinModule *module) { - auto procedures_cb = [all_modules](mgp_list * /*unused*/, mgp_graph * /*unused*/, mgp_result *result, - mgp_memory *memory) { + auto transformations_cb = [all_modules](mgp_list * /*unused*/, mgp_graph * /*unused*/, mgp_result *result, + mgp_memory *memory) { for (const auto &[module_name, module] : *all_modules) { // Return the results in sorted order by module and by transformation. static_assert( @@ -225,7 +238,7 @@ void RegisterMgTransformations(const std::map(mgp_type_string)) == MGP_ERROR_NO_ERROR); module->AddProcedure("transformations", std::move(procedures)); } diff --git a/src/query/procedure/py_module.cpp b/src/query/procedure/py_module.cpp index 9d71f5130..e41c987e1 100644 --- a/src/query/procedure/py_module.cpp +++ b/src/query/procedure/py_module.cpp @@ -1,15 +1,20 @@ #include "query/procedure/py_module.hpp" +#include +#include #include #include #include +#include #include "query/procedure/mg_procedure_helpers.hpp" #include "query/procedure/mg_procedure_impl.hpp" +#include "utils/on_scope_exit.hpp" #include "utils/pmr/vector.hpp" namespace query::procedure { +namespace { // Set this as a __reduce__ special method on our types to prevent `pickle` and // `copy` module operations on our types. PyObject *DisallowPickleAndCopy(PyObject *self, PyObject *Py_UNUSED(ignored)) { @@ -21,6 +26,92 @@ PyObject *DisallowPickleAndCopy(PyObject *self, PyObject *Py_UNUSED(ignored)) { return nullptr; } +PyObject *gMgpUnknownError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpUnableToAllocateError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpInsufficientBufferError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpOutOfRangeError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpLogicErrorError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpDeletedObjectError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpInvalidArgumentError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpKeyAlreadyExistsError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpImmutableObjectError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpValueConversionError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpSerializationError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +// Returns true if an exception is raised +bool RaiseExceptionFromErrorCode(const mgp_error error) { + switch (error) { + case MGP_ERROR_NO_ERROR: + return false; + case MGP_ERROR_UNKNOWN_ERROR: { + PyErr_SetString(gMgpUnknownError, "Unknown error happened."); + return true; + } + case MGP_ERROR_UNABLE_TO_ALLOCATE: { + PyErr_SetString(gMgpUnableToAllocateError, "Unable to allocate memory."); + return true; + } + case MGP_ERROR_INSUFFICIENT_BUFFER: { + PyErr_SetString(gMgpInsufficientBufferError, "Insufficient buffer."); + return true; + } + case MGP_ERROR_OUT_OF_RANGE: { + PyErr_SetString(gMgpOutOfRangeError, "Out of range."); + return true; + } + case MGP_ERROR_LOGIC_ERROR: { + PyErr_SetString(gMgpLogicErrorError, "Logic error."); + return true; + } + case MGP_ERROR_DELETED_OBJECT: { + PyErr_SetString(gMgpDeletedObjectError, "Accessing deleted object."); + return true; + } + case MGP_ERROR_INVALID_ARGUMENT: { + PyErr_SetString(gMgpInvalidArgumentError, "Invalid argument."); + return true; + } + case MGP_ERROR_KEY_ALREADY_EXISTS: { + PyErr_SetString(gMgpKeyAlreadyExistsError, "Key already exists."); + return true; + } + case MGP_ERROR_IMMUTABLE_OBJECT: { + PyErr_SetString(gMgpImmutableObjectError, "Cannot modify immutable object."); + return true; + } + case MGP_ERROR_VALUE_CONVERSION: { + PyErr_SetString(gMgpValueConversionError, "Value conversion failed."); + return true; + } + case MGP_ERROR_SERIALIZATION_ERROR: { + PyErr_SetString(gMgpSerializationError, "Operation cannot be serialized."); + return true; + } + } +} + +mgp_value *PyObjectToMgpValueWithPythonExceptions(PyObject *py_value, mgp_memory *memory) noexcept { + try { + return PyObjectToMgpValue(py_value, memory); + } catch (const std::bad_alloc &e) { + PyErr_SetString(PyExc_MemoryError, e.what()); + return nullptr; + } catch (const std::overflow_error &e) { + PyErr_SetString(PyExc_OverflowError, e.what()); + return nullptr; + } catch (const std::invalid_argument &e) { + PyErr_SetString(PyExc_ValueError, e.what()); + return nullptr; + } catch (const std::exception &e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return nullptr; + } catch (...) { + PyErr_SetString(PyExc_RuntimeError, "Unknown error happened"); + return nullptr; + } +} +} // namespace + // Definitions of types wrapping C API types // // These should all be in the private `_mgp` Python module, which will be used @@ -71,8 +162,13 @@ PyObject *PyVerticesIteratorGet(PyVerticesIterator *self, PyObject *Py_UNUSED(ig MG_ASSERT(self->it); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - auto *vertex = Call(mgp_vertices_iterator_get, self->it); - if (!vertex) Py_RETURN_NONE; + mgp_vertex *vertex{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_vertices_iterator_get(self->it, &vertex))) { + return nullptr; + } + if (vertex == nullptr) { + return Py_None; + } return MakePyVertex(*vertex, self->py_graph); } @@ -80,8 +176,13 @@ PyObject *PyVerticesIteratorNext(PyVerticesIterator *self, PyObject *Py_UNUSED(i MG_ASSERT(self->it); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - auto *vertex = Call(mgp_vertices_iterator_next, self->it); - if (!vertex) Py_RETURN_NONE; + mgp_vertex *vertex{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_vertices_iterator_next(self->it, &vertex))) { + return nullptr; + } + if (vertex == nullptr) { + return Py_None; + } return MakePyVertex(*vertex, self->py_graph); } @@ -131,8 +232,13 @@ PyObject *PyEdgesIteratorGet(PyEdgesIterator *self, PyObject *Py_UNUSED(ignored) MG_ASSERT(self->it); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - auto *edge = Call(mgp_edges_iterator_get, self->it); - if (!edge) Py_RETURN_NONE; + mgp_edge *edge{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_edges_iterator_get(self->it, &edge))) { + return nullptr; + } + if (edge == nullptr) { + return Py_None; + } return MakePyEdge(*edge, self->py_graph); } @@ -140,8 +246,13 @@ PyObject *PyEdgesIteratorNext(PyEdgesIterator *self, PyObject *Py_UNUSED(ignored MG_ASSERT(self->it); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - auto *edge = Call(mgp_edges_iterator_next, self->it); - if (!edge) Py_RETURN_NONE; + mgp_edge *edge{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_edges_iterator_next(self->it, &edge))) { + return nullptr; + } + if (edge == nullptr) { + return Py_None; + } return MakePyEdge(*edge, self->py_graph); } @@ -172,32 +283,64 @@ PyObject *PyGraphInvalidate(PyGraph *self, PyObject *Py_UNUSED(ignored)) { Py_RETURN_NONE; } -PyObject *PyGraphIsValid(PyGraph *self, PyObject *Py_UNUSED(ignored)) { return PyBool_FromLong(!!self->graph); } +bool PyGraphIsValidImpl(PyGraph &self) { return self.graph != nullptr; } -PyObject *MakePyVertex(mgp_vertex *vertex, PyGraph *py_graph); +PyObject *PyGraphIsValid(PyGraph *self, PyObject *Py_UNUSED(ignored)) { + return PyBool_FromLong(PyGraphIsValidImpl(*self)); +} + +PyObject *PyGraphIsMutable(PyGraph *self, PyObject *Py_UNUSED(ignored)) { + return PyBool_FromLong(CallBool(mgp_graph_is_mutable, self->graph)); +} + +PyObject *MakePyVertexWithoutCopy(mgp_vertex &vertex, PyGraph *py_graph); PyObject *PyGraphGetVertexById(PyGraph *self, PyObject *args) { - MG_ASSERT(self->graph); + MG_ASSERT(PyGraphIsValidImpl(*self)); MG_ASSERT(self->memory); static_assert(std::is_same_v); int64_t id = 0; if (!PyArg_ParseTuple(args, "l", &id)) return nullptr; - auto *vertex = Call(mgp_graph_get_vertex_by_id, self->graph, mgp_vertex_id{id}, self->memory); + mgp_vertex *vertex{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_graph_get_vertex_by_id(self->graph, mgp_vertex_id{id}, self->memory, &vertex))) { + return nullptr; + } if (!vertex) { PyErr_SetString(PyExc_IndexError, "Unable to find the vertex with given ID."); return nullptr; } - auto *py_vertex = MakePyVertex(vertex, self); + auto *py_vertex = MakePyVertexWithoutCopy(*vertex, self); if (!py_vertex) mgp_vertex_destroy(vertex); return py_vertex; } -PyObject *PyGraphIterVertices(PyGraph *self, PyObject *Py_UNUSED(ignored)) { - MG_ASSERT(self->graph); +PyObject *PyGraphCreateVertex(PyGraph *self, PyObject *Py_UNUSED(ignored)) { + MG_ASSERT(PyGraphIsValidImpl(*self)); MG_ASSERT(self->memory); - auto *vertices_it = Call(mgp_graph_iter_vertices, self->graph, self->memory); - if (!vertices_it) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_vertices_iterator."); + MgpUniquePtr new_vertex{nullptr, mgp_vertex_destroy}; + if (RaiseExceptionFromErrorCode(CreateMgpObject(new_vertex, mgp_graph_create_vertex, self->graph, self->memory))) { + return nullptr; + } + auto *py_vertex = MakePyVertexWithoutCopy(*new_vertex, self); + if (py_vertex != nullptr) { + static_cast(new_vertex.release()); + } + return py_vertex; +} + +PyObject *PyGraphCreateEdge(PyGraph *self, PyObject *args); + +PyObject *PyGraphDeleteVertex(PyGraph *self, PyObject *args); + +PyObject *PyGraphDetachDeleteVertex(PyGraph *self, PyObject *args); + +PyObject *PyGraphDeleteEdge(PyGraph *self, PyObject *args); + +PyObject *PyGraphIterVertices(PyGraph *self, PyObject *Py_UNUSED(ignored)) { + MG_ASSERT(PyGraphIsValidImpl(*self)); + MG_ASSERT(self->memory); + mgp_vertices_iterator *vertices_it{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_graph_iter_vertices(self->graph, self->memory, &vertices_it))) { return nullptr; } auto *py_vertices_it = PyObject_New(PyVerticesIterator, &PyVerticesIteratorType); @@ -212,7 +355,7 @@ PyObject *PyGraphIterVertices(PyGraph *self, PyObject *Py_UNUSED(ignored)) { } PyObject *PyGraphMustAbort(PyGraph *self, PyObject *Py_UNUSED(ignored)) { - MG_ASSERT(self->graph); + MG_ASSERT(PyGraphIsValidImpl(*self)); return PyBool_FromLong(mgp_must_abort(self->graph)); } @@ -222,8 +365,16 @@ static PyMethodDef PyGraphMethods[] = { "Invalidate the Graph context thus preventing the Graph from being used."}, {"is_valid", reinterpret_cast(PyGraphIsValid), METH_NOARGS, "Return True if Graph is in valid context and may be used."}, + {"is_mutable", reinterpret_cast(PyGraphIsMutable), METH_NOARGS, + "Return True if Graph is mutable and can be used to modify vertices and edges."}, {"get_vertex_by_id", reinterpret_cast(PyGraphGetVertexById), METH_VARARGS, "Get the vertex or raise IndexError."}, + {"create_vertex", reinterpret_cast(PyGraphCreateVertex), METH_NOARGS, "Create a vertex."}, + {"create_edge", reinterpret_cast(PyGraphCreateEdge), METH_VARARGS, "Create an edge."}, + {"delete_vertex", reinterpret_cast(PyGraphDeleteVertex), METH_VARARGS, "Delete a vertex."}, + {"detach_delete_vertex", reinterpret_cast(PyGraphDetachDeleteVertex), METH_VARARGS, + "Delete a vertex and all of its edges."}, + {"delete_edge", reinterpret_cast(PyGraphDeleteEdge), METH_VARARGS, "Delete an edge."}, {"iter_vertices", reinterpret_cast(PyGraphIterVertices), METH_NOARGS, "Return _mgp.VerticesIterator."}, {"must_abort", reinterpret_cast(PyGraphMustAbort), METH_NOARGS, "Check whether the running procedure should abort"}, @@ -285,15 +436,10 @@ struct PyQueryProc { PyObject *PyQueryProcAddArg(PyQueryProc *self, PyObject *args) { MG_ASSERT(self->proc); const char *name = nullptr; - PyObject *py_type = nullptr; - if (!PyArg_ParseTuple(args, "sO", &name, &py_type)) return nullptr; - if (Py_TYPE(py_type) != &PyCypherTypeType) { - PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Type."); - return nullptr; - } - auto *type = reinterpret_cast(py_type)->type; - if (mgp_proc_add_arg(self->proc, name, type) != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_ValueError, "Invalid call to mgp_proc_add_arg."); + PyCypherType *py_type = nullptr; + if (!PyArg_ParseTuple(args, "sO!", &name, &PyCypherTypeType, &py_type)) return nullptr; + auto *type = py_type->type; + if (RaiseExceptionFromErrorCode(mgp_proc_add_arg(self->proc, name, type))) { return nullptr; } Py_RETURN_NONE; @@ -302,35 +448,17 @@ PyObject *PyQueryProcAddArg(PyQueryProc *self, PyObject *args) { PyObject *PyQueryProcAddOptArg(PyQueryProc *self, PyObject *args) { MG_ASSERT(self->proc); const char *name = nullptr; - PyObject *py_type = nullptr; + PyCypherType *py_type = nullptr; PyObject *py_value = nullptr; - if (!PyArg_ParseTuple(args, "sOO", &name, &py_type, &py_value)) return nullptr; - if (Py_TYPE(py_type) != &PyCypherTypeType) { - PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Type."); - return nullptr; - } - auto *type = reinterpret_cast(py_type)->type; + if (!PyArg_ParseTuple(args, "sO!O", &name, &PyCypherTypeType, &py_type, &py_value)) return nullptr; + auto *type = py_type->type; mgp_memory memory{self->proc->opt_args.get_allocator().GetMemoryResource()}; - mgp_value *value{nullptr}; - try { - value = PyObjectToMgpValue(py_value, &memory); - } catch (const std::bad_alloc &e) { - PyErr_SetString(PyExc_MemoryError, e.what()); - return nullptr; - } catch (const std::overflow_error &e) { - PyErr_SetString(PyExc_OverflowError, e.what()); - return nullptr; - } catch (const std::invalid_argument &e) { - PyErr_SetString(PyExc_ValueError, e.what()); - return nullptr; - } catch (const std::exception &e) { - PyErr_SetString(PyExc_RuntimeError, e.what()); + mgp_value *value = PyObjectToMgpValueWithPythonExceptions(py_value, &memory); + if (value == nullptr) { return nullptr; } - MG_ASSERT(value); - if (mgp_proc_add_opt_arg(self->proc, name, type, value) != MGP_ERROR_NO_ERROR) { + if (RaiseExceptionFromErrorCode(mgp_proc_add_opt_arg(self->proc, name, type, value))) { mgp_value_destroy(value); - PyErr_SetString(PyExc_ValueError, "Invalid call to mgp_proc_add_opt_arg."); return nullptr; } mgp_value_destroy(value); @@ -340,15 +468,11 @@ PyObject *PyQueryProcAddOptArg(PyQueryProc *self, PyObject *args) { PyObject *PyQueryProcAddResult(PyQueryProc *self, PyObject *args) { MG_ASSERT(self->proc); const char *name = nullptr; - PyObject *py_type = nullptr; - if (!PyArg_ParseTuple(args, "sO", &name, &py_type)) return nullptr; - if (Py_TYPE(py_type) != &PyCypherTypeType) { - PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Type."); - return nullptr; - } + PyCypherType *py_type = nullptr; + if (!PyArg_ParseTuple(args, "sO!", &name, &PyCypherTypeType, &py_type)) return nullptr; + auto *type = reinterpret_cast(py_type)->type; - if (mgp_proc_add_result(self->proc, name, type) != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_ValueError, "Invalid call to mgp_proc_add_result."); + if (RaiseExceptionFromErrorCode(mgp_proc_add_result(self->proc, name, type))) { return nullptr; } Py_RETURN_NONE; @@ -357,15 +481,10 @@ PyObject *PyQueryProcAddResult(PyQueryProc *self, PyObject *args) { PyObject *PyQueryProcAddDeprecatedResult(PyQueryProc *self, PyObject *args) { MG_ASSERT(self->proc); const char *name = nullptr; - PyObject *py_type = nullptr; - if (!PyArg_ParseTuple(args, "sO", &name, &py_type)) return nullptr; - if (Py_TYPE(py_type) != &PyCypherTypeType) { - PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Type."); - return nullptr; - } + PyCypherType *py_type = nullptr; + if (!PyArg_ParseTuple(args, "sO!", &name, &PyCypherTypeType, &py_type)) return nullptr; auto *type = reinterpret_cast(py_type)->type; - if (const auto err = mgp_proc_add_deprecated_result(self->proc, name, type); err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_ValueError, "Invalid call to mgp_proc_add_deprecated_result."); + if (RaiseExceptionFromErrorCode(mgp_proc_add_deprecated_result(self->proc, name, type))) { return nullptr; } Py_RETURN_NONE; @@ -425,8 +544,14 @@ PyObject *PyMessageIsValid(PyMessage *self, PyObject *Py_UNUSED(ignored)) { PyObject *PyMessageGetPayload(PyMessage *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->message); - auto payload_size = Call(mgp_message_payload_size, self->message); - const auto *payload = Call(mgp_message_payload, self->message); + size_t payload_size{0}; + if (RaiseExceptionFromErrorCode(mgp_message_payload_size(self->message, &payload_size))) { + return nullptr; + } + const char *payload{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_message_payload(self->message, &payload))) { + return nullptr; + } auto *raw_bytes = PyByteArray_FromStringAndSize(payload, payload_size); if (!raw_bytes) { PyErr_SetString(PyExc_RuntimeError, "Unable to get raw bytes from payload"); @@ -438,7 +563,10 @@ PyObject *PyMessageGetPayload(PyMessage *self, PyObject *Py_UNUSED(ignored)) { PyObject *PyMessageGetTopicName(PyMessage *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->message); MG_ASSERT(self->memory); - const auto *topic_name = Call(mgp_message_topic_name, self->message); + const char *topic_name{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_message_topic_name(self->message, &topic_name))) { + return nullptr; + } auto *py_topic_name = PyUnicode_FromString(topic_name); if (!py_topic_name) { PyErr_SetString(PyExc_RuntimeError, "Unable to get raw bytes from payload"); @@ -450,8 +578,14 @@ PyObject *PyMessageGetTopicName(PyMessage *self, PyObject *Py_UNUSED(ignored)) { PyObject *PyMessageGetKey(PyMessage *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->message); MG_ASSERT(self->memory); - auto key_size = Call(mgp_message_key_size, self->message); - const auto *key = Call(mgp_message_key, self->message); + size_t key_size{0}; + if (RaiseExceptionFromErrorCode(mgp_message_key_size(self->message, &key_size))) { + return nullptr; + } + const char *key{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_message_key(self->message, &key))) { + return nullptr; + } auto *raw_bytes = PyByteArray_FromStringAndSize(key, key_size); if (!raw_bytes) { PyErr_SetString(PyExc_RuntimeError, "Unable to get raw bytes from payload"); @@ -463,7 +597,10 @@ PyObject *PyMessageGetKey(PyMessage *self, PyObject *Py_UNUSED(ignored)) { PyObject *PyMessageGetTimestamp(PyMessage *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->message); MG_ASSERT(self->memory); - auto timestamp = Call(mgp_message_timestamp, self->message); + int64_t timestamp{0}; + if (RaiseExceptionFromErrorCode(mgp_message_timestamp(self->message, ×tamp))) { + return nullptr; + } auto *py_int = PyLong_FromUnsignedLong(timestamp); if (!py_int) { PyErr_SetString(PyExc_IndexError, "Unable to get timestamp."); @@ -518,7 +655,7 @@ PyObject *PyMessagesGetTotalMessages(PyMessages *self, PyObject *Py_UNUSED(ignor auto size = self->messages->messages.size(); auto *py_int = PyLong_FromSize_t(size); if (!py_int) { - PyErr_SetString(PyExc_IndexError, "Unable to get timestamp."); + PyErr_SetString(PyExc_IndexError, "Unable to get total messages count."); return nullptr; } return py_int; @@ -630,9 +767,8 @@ std::optional AddRecordFromPython(mgp_result *result, py::Obj } py::Object items(PyDict_Items(fields.Ptr())); if (!items) return py::FetchError(); - auto *record = Call(mgp_result_new_record, result); - if (!record) { - PyErr_NoMemory(); + mgp_result_record *record{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_result_new_record(result, &record))) { return py::FetchError(); } Py_ssize_t len = PyList_GET_SIZE(items.Ptr()); @@ -654,15 +790,10 @@ std::optional AddRecordFromPython(mgp_result *result, py::Obj auto *val = PyTuple_GetItem(item, 1); if (!val) return py::FetchError(); mgp_memory memory{result->rows.get_allocator().GetMemoryResource()}; - mgp_value *field_val{nullptr}; - try { - // TODO: Make PyObjectToMgpValue set a Python exception instead. - field_val = PyObjectToMgpValue(val, &memory); - } catch (const std::exception &e) { - PyErr_SetString(PyExc_ValueError, e.what()); + mgp_value *field_val = PyObjectToMgpValueWithPythonExceptions(val, &memory); + if (field_val == nullptr) { return py::FetchError(); } - MG_ASSERT(field_val); if (mgp_result_record_insert(record, field_name, field_val) != MGP_ERROR_NO_ERROR) { std::stringstream ss; ss << "Unable to insert field '" << py::Object::FromBorrow(key) << "' with value: '" @@ -849,9 +980,8 @@ void CallPythonTransformation(const py::Object &py_cb, mgp_messages *msgs, mgp_g static_cast(mgp_result_set_error_msg(result, maybe_msg->c_str())); } } -} // namespace -PyObject *PyQueryModuleAddReadProcedure(PyQueryModule *self, PyObject *cb) { +PyObject *PyQueryModuleAddProcedure(PyQueryModule *self, PyObject *cb, bool is_write_procedure) { MG_ASSERT(self->module); if (!PyCallable_Check(cb)) { PyErr_SetString(PyExc_TypeError, "Expected a callable object."); @@ -871,7 +1001,7 @@ PyObject *PyQueryModuleAddReadProcedure(PyQueryModule *self, PyObject *cb) { [py_cb](mgp_list *args, mgp_graph *graph, mgp_result *result, mgp_memory *memory) { CallPythonProcedure(py_cb, args, graph, result, memory); }, - memory, false); + memory, is_write_procedure); const auto &[proc_it, did_insert] = self->module->procedures.emplace(name, std::move(proc)); if (!did_insert) { PyErr_SetString(PyExc_ValueError, "Already registered a procedure with the same name."); @@ -882,6 +1012,15 @@ PyObject *PyQueryModuleAddReadProcedure(PyQueryModule *self, PyObject *cb) { py_proc->proc = &proc_it->second; return reinterpret_cast(py_proc); } +} // namespace + +PyObject *PyQueryModuleAddReadProcedure(PyQueryModule *self, PyObject *cb) { + return PyQueryModuleAddProcedure(self, cb, false); +} + +PyObject *PyQueryModuleAddWriteProcedure(PyQueryModule *self, PyObject *cb) { + return PyQueryModuleAddProcedure(self, cb, true); +} PyObject *PyQueryModuleAddTransformation(PyQueryModule *self, PyObject *cb) { MG_ASSERT(self->module); @@ -916,6 +1055,8 @@ static PyMethodDef PyQueryModuleMethods[] = { {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"}, {"add_read_procedure", reinterpret_cast(PyQueryModuleAddReadProcedure), METH_O, "Register a read-only procedure with this module."}, + {"add_write_procedure", reinterpret_cast(PyQueryModuleAddWriteProcedure), METH_O, + "Register a writeable procedure with this module."}, {"add_transformation", reinterpret_cast(PyQueryModuleAddTransformation), METH_O, "Register a transformation with this module."}, {nullptr}, @@ -946,7 +1087,11 @@ PyObject *PyMgpModuleTypeNullable(PyObject *mod, PyObject *obj) { return nullptr; } auto *py_type = reinterpret_cast(obj); - return MakePyCypherType(Call(mgp_type_nullable, py_type->type)); + mgp_type *type{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_type_nullable(py_type->type, &type))) { + return nullptr; + } + return MakePyCypherType(type); } PyObject *PyMgpModuleTypeList(PyObject *mod, PyObject *obj) { @@ -955,48 +1100,33 @@ PyObject *PyMgpModuleTypeList(PyObject *mod, PyObject *obj) { return nullptr; } auto *py_type = reinterpret_cast(obj); - return MakePyCypherType(Call(mgp_type_list, py_type->type)); + mgp_type *type{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_type_list(py_type->type, &type))) { + return nullptr; + } + return MakePyCypherType(type); } -PyObject *PyMgpModuleTypeAny(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_any)); -} +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define DEFINE_PY_MGP_MODULE_TYPE(capital_type, small_type) \ + PyObject *PyMgpModuleType##capital_type(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { \ + mgp_type *type{nullptr}; \ + if (RaiseExceptionFromErrorCode(mgp_type_##small_type(&type))) { \ + return nullptr; \ + } \ + return MakePyCypherType(type); \ + } -PyObject *PyMgpModuleTypeBool(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_bool)); -} - -PyObject *PyMgpModuleTypeString(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_string)); -} - -PyObject *PyMgpModuleTypeInt(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_int)); -} - -PyObject *PyMgpModuleTypeFloat(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_float)); -} - -PyObject *PyMgpModuleTypeNumber(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_number)); -} - -PyObject *PyMgpModuleTypeMap(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_map)); -} - -PyObject *PyMgpModuleTypeNode(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_node)); -} - -PyObject *PyMgpModuleTypeRelationship(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_relationship)); -} - -PyObject *PyMgpModuleTypePath(PyObject * /*mod*/, PyObject *Py_UNUSED(ignored)) { - return MakePyCypherType(Call(mgp_type_path)); -} +DEFINE_PY_MGP_MODULE_TYPE(Any, any); +DEFINE_PY_MGP_MODULE_TYPE(Bool, bool); +DEFINE_PY_MGP_MODULE_TYPE(String, string); +DEFINE_PY_MGP_MODULE_TYPE(Int, int); +DEFINE_PY_MGP_MODULE_TYPE(Float, float); +DEFINE_PY_MGP_MODULE_TYPE(Number, number); +DEFINE_PY_MGP_MODULE_TYPE(Map, map); +DEFINE_PY_MGP_MODULE_TYPE(Node, node); +DEFINE_PY_MGP_MODULE_TYPE(Relationship, relationship); +DEFINE_PY_MGP_MODULE_TYPE(Path, path); static PyMethodDef PyMgpModuleMethods[] = { {"type_nullable", PyMgpModuleTypeNullable, METH_O, @@ -1051,8 +1181,13 @@ PyObject *PyPropertiesIteratorGet(PyPropertiesIterator *self, PyObject *Py_UNUSE MG_ASSERT(self->it); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - auto *property = Call(mgp_properties_iterator_get, self->it); - if (!property) Py_RETURN_NONE; + mgp_property *property{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_properties_iterator_get(self->it, &property))) { + return nullptr; + } + if (property == nullptr) { + return Py_None; + } py::Object py_name(PyUnicode_FromString(property->name)); if (!py_name) return nullptr; auto py_value = MgpValueToPyObject(*property->value, self->py_graph); @@ -1064,8 +1199,13 @@ PyObject *PyPropertiesIteratorNext(PyPropertiesIterator *self, PyObject *Py_UNUS MG_ASSERT(self->it); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - auto *property = Call(mgp_properties_iterator_next, self->it); - if (!property) Py_RETURN_NONE; + mgp_property *property{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_properties_iterator_next(self->it, &property))) { + return nullptr; + } + if (property == nullptr) { + return Py_None; + } py::Object py_name(PyUnicode_FromString(property->name)); if (!py_name) return nullptr; auto py_value = MgpValueToPyObject(*property->value, self->py_graph); @@ -1106,7 +1246,11 @@ PyObject *PyEdgeGetTypeName(PyEdge *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->edge); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - return PyUnicode_FromString(Call(mgp_edge_get_type, self->edge).name); + mgp_edge_type edge_type{nullptr}; + if (RaiseExceptionFromErrorCode(mgp_edge_get_type(self->edge, &edge_type))) { + return nullptr; + } + return PyUnicode_FromString(edge_type.name); } PyObject *PyEdgeFromVertex(PyEdge *self, PyObject *Py_UNUSED(ignored)) { @@ -1114,8 +1258,7 @@ PyObject *PyEdgeFromVertex(PyEdge *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->edge); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - auto &vertex = self->edge->from; - return MakePyVertex(&vertex, self->py_graph); + return MakePyVertex(self->edge->from, self->py_graph); } PyObject *PyEdgeToVertex(PyEdge *self, PyObject *Py_UNUSED(ignored)) { @@ -1123,8 +1266,7 @@ PyObject *PyEdgeToVertex(PyEdge *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->edge); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - auto &vertex = self->edge->to; - return MakePyVertex(vertex, self->py_graph); + return MakePyVertex(self->edge->to, self->py_graph); } void PyEdgeDealloc(PyEdge *self) { @@ -1142,12 +1284,24 @@ PyObject *PyEdgeIsValid(PyEdge *self, PyObject *Py_UNUSED(ignored)) { return PyBool_FromLong(self->py_graph && self->py_graph->graph); } +PyObject *PyEdgeUnderlyingGraphIsMutable(PyEdge *self, PyObject *Py_UNUSED(ignored)) { + MG_ASSERT(self); + MG_ASSERT(self->edge); + MG_ASSERT(self->py_graph); + MG_ASSERT(self->py_graph->graph); + return PyBool_FromLong(CallBool(mgp_graph_is_mutable, self->py_graph->graph)); +} + PyObject *PyEdgeGetId(PyEdge *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self); MG_ASSERT(self->edge); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - return PyLong_FromLongLong(Call(mgp_edge_get_id, self->edge).as_int); + mgp_edge_id edge_id{0}; + if (RaiseExceptionFromErrorCode(mgp_edge_get_id(self->edge, &edge_id))) { + return nullptr; + } + return PyLong_FromLongLong(edge_id.as_int); } PyObject *PyEdgeIterProperties(PyEdge *self, PyObject *Py_UNUSED(ignored)) { @@ -1156,12 +1310,7 @@ PyObject *PyEdgeIterProperties(PyEdge *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); mgp_properties_iterator *properties_it{nullptr}; - if (const auto err = mgp_edge_iter_properties(self->edge, self->py_graph->memory, &properties_it); - err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_properties_iterator."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting mgp_properties_iterator."); + if (RaiseExceptionFromErrorCode(mgp_edge_iter_properties(self->edge, self->py_graph->memory, &properties_it))) { return nullptr; } auto *py_properties_it = PyObject_New(PyPropertiesIterator, &PyPropertiesIteratorType); @@ -1183,12 +1332,7 @@ PyObject *PyEdgeGetProperty(PyEdge *self, PyObject *args) { const char *prop_name = nullptr; if (!PyArg_ParseTuple(args, "s", &prop_name)) return nullptr; mgp_value *prop_value{nullptr}; - if (const auto err = mgp_edge_get_property(self->edge, prop_name, self->py_graph->memory, &prop_value); - err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_value for edge property value."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting mgp_edge property."); + if (RaiseExceptionFromErrorCode(mgp_edge_get_property(self->edge, prop_name, self->py_graph->memory, &prop_value))) { return nullptr; } auto py_prop_value = MgpValueToPyObject(*prop_value, self->py_graph); @@ -1196,10 +1340,32 @@ PyObject *PyEdgeGetProperty(PyEdge *self, PyObject *args) { return py_prop_value.Steal(); } +PyObject *PyEdgeSetProperty(PyEdge *self, PyObject *args) { + MG_ASSERT(self); + MG_ASSERT(self->edge); + MG_ASSERT(self->py_graph); + MG_ASSERT(self->py_graph->graph); + const char *prop_name = nullptr; + PyObject *py_value{nullptr}; + if (!PyArg_ParseTuple(args, "sO", &prop_name, &py_value)) { + return nullptr; + } + MgpUniquePtr prop_value{PyObjectToMgpValueWithPythonExceptions(py_value, self->py_graph->memory), + mgp_value_destroy}; + + if (prop_value == nullptr || + RaiseExceptionFromErrorCode(mgp_edge_set_property(self->edge, prop_name, prop_value.get()))) { + return nullptr; + } + Py_RETURN_NONE; +} + static PyMethodDef PyEdgeMethods[] = { {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported."}, {"is_valid", reinterpret_cast(PyEdgeIsValid), METH_NOARGS, - "Return True if Edge is in valid context and may be used."}, + "Return True if the edge is in valid context and may be used."}, + {"underlying_graph_is_mutable", reinterpret_cast(PyEdgeUnderlyingGraphIsMutable), METH_NOARGS, + "Return True if the edge is mutable and can be modified."}, {"get_id", reinterpret_cast(PyEdgeGetId), METH_NOARGS, "Return edge id."}, {"get_type_name", reinterpret_cast(PyEdgeGetTypeName), METH_NOARGS, "Return the edge's type name."}, {"from_vertex", reinterpret_cast(PyEdgeFromVertex), METH_NOARGS, "Return the edge's source vertex."}, @@ -1208,6 +1374,8 @@ static PyMethodDef PyEdgeMethods[] = { "Return _mgp.PropertiesIterator for this edge."}, {"get_property", reinterpret_cast(PyEdgeGetProperty), METH_VARARGS, "Return edge property with given name."}, + {"set_property", reinterpret_cast(PyEdgeSetProperty), METH_VARARGS, + "Set the value of the property on the edge."}, {nullptr}, }; @@ -1226,6 +1394,18 @@ static PyTypeObject PyEdgeType = { }; // clang-format on +PyObject *MakePyEdgeWithoutCopy(mgp_edge &edge, PyGraph *py_graph) { + MG_ASSERT(py_graph); + MG_ASSERT(py_graph->graph && py_graph->memory); + MG_ASSERT(edge.GetMemoryResource() == py_graph->memory->impl); + auto *py_edge = PyObject_New(PyEdge, &PyEdgeType); // NOLINT(cppcoreguidelines-pro-type-cstyle-cast) + if (!py_edge) return nullptr; + py_edge->edge = &edge; + py_edge->py_graph = py_graph; + Py_INCREF(py_graph); // NOLINT(cppcoreguidelines-pro-type-cstyle-cast) + return reinterpret_cast(py_edge); +} + /// Create an instance of `_mgp.Edge` class. /// /// The created instance references an existing `_mgp.Graph` instance, which @@ -1233,24 +1413,15 @@ static PyTypeObject PyEdgeType = { PyObject *MakePyEdge(mgp_edge &edge, PyGraph *py_graph) { MG_ASSERT(py_graph); MG_ASSERT(py_graph->graph && py_graph->memory); - mgp_edge *edge_copy{nullptr}; - // TODO(antaljanosbenjamin) - if (const auto err = mgp_edge_copy(&edge, py_graph->memory, &edge_copy); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_edge."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during creating mgp_edge"); + MgpUniquePtr edge_copy{nullptr, mgp_edge_destroy}; + if (RaiseExceptionFromErrorCode(CreateMgpObject(edge_copy, mgp_edge_copy, &edge, py_graph->memory))) { return nullptr; } - auto *py_edge = PyObject_New(PyEdge, &PyEdgeType); - if (!py_edge) { - mgp_edge_destroy(edge_copy); - return nullptr; + auto *py_edge = MakePyEdgeWithoutCopy(*edge_copy, py_graph); + if (py_edge != nullptr) { + static_cast(edge_copy.release()); } - py_edge->edge = edge_copy; - py_edge->py_graph = py_graph; - Py_INCREF(py_graph); - return reinterpret_cast(py_edge); + return py_edge; } PyObject *PyEdgeRichCompare(PyObject *self, PyObject *other, int op) { @@ -1265,13 +1436,7 @@ PyObject *PyEdgeRichCompare(PyObject *self, PyObject *other, int op) { auto *e2 = reinterpret_cast(other); MG_ASSERT(e1->edge); MG_ASSERT(e2->edge); - - int equals{0}; - if (const auto err = mgp_edge_equal(e1->edge, e2->edge, &equals); err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during comparing edges"); - return nullptr; - } - return PyBool_FromLong(equals); + return PyBool_FromLong(Call(mgp_edge_equal, e1->edge, e2->edge)); } // clang-format off @@ -1297,14 +1462,21 @@ PyObject *PyVertexIsValid(PyVertex *self, PyObject *Py_UNUSED(ignored)) { return PyBool_FromLong(self->py_graph && self->py_graph->graph); } +PyObject *PyVertexUnderlyingGraphIsMutable(PyVertex *self, PyObject *Py_UNUSED(ignored)) { + MG_ASSERT(self); + MG_ASSERT(self->vertex); + MG_ASSERT(self->py_graph); + MG_ASSERT(self->py_graph->graph); + return PyBool_FromLong(CallBool(mgp_graph_is_mutable, self->py_graph->graph)); +} + PyObject *PyVertexGetId(PyVertex *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self); MG_ASSERT(self->vertex); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); mgp_vertex_id id{}; - if (const auto err = mgp_vertex_get_id(self->vertex, &id); err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting id of mgp_vertex"); + if (RaiseExceptionFromErrorCode(mgp_vertex_get_id(self->vertex, &id))) { return nullptr; } return PyLong_FromLongLong(id.as_int); @@ -1316,8 +1488,7 @@ PyObject *PyVertexLabelsCount(PyVertex *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); size_t label_count{0}; - if (const auto err = mgp_vertex_labels_count(self->vertex, &label_count); err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting label count of mgp_vertex"); + if (RaiseExceptionFromErrorCode(mgp_vertex_labels_count(self->vertex, &label_count))) { return nullptr; } return PyLong_FromSize_t(label_count); @@ -1334,12 +1505,7 @@ PyObject *PyVertexLabelAt(PyVertex *self, PyObject *args) { return nullptr; } mgp_label label{nullptr}; - if (const auto err = mgp_vertex_label_at(self->vertex, id, &label); err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting label of mgp_vertex"); - return nullptr; - } - if (label.name == nullptr || id < 0) { - PyErr_SetString(PyExc_IndexError, "Unable to find the label with given ID."); + if (RaiseExceptionFromErrorCode(mgp_vertex_label_at(self->vertex, id, &label))) { return nullptr; } return PyUnicode_FromString(label.name); @@ -1351,12 +1517,7 @@ PyObject *PyVertexIterInEdges(PyVertex *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); mgp_edges_iterator *edges_it{nullptr}; - if (const auto err = mgp_vertex_iter_in_edges(self->vertex, self->py_graph->memory, &edges_it); - err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_edges_iterator for in edges."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting mgp_edges_iterator for in edges"); + if (RaiseExceptionFromErrorCode(mgp_vertex_iter_in_edges(self->vertex, self->py_graph->memory, &edges_it))) { return nullptr; } auto *py_edges_it = PyObject_New(PyEdgesIterator, &PyEdgesIteratorType); @@ -1376,12 +1537,7 @@ PyObject *PyVertexIterOutEdges(PyVertex *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); mgp_edges_iterator *edges_it{nullptr}; - if (const auto err = mgp_vertex_iter_out_edges(self->vertex, self->py_graph->memory, &edges_it); - err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_edges_iterator for out edges."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting mgp_edges_iterator for out edges"); + if (RaiseExceptionFromErrorCode(mgp_vertex_iter_out_edges(self->vertex, self->py_graph->memory, &edges_it))) { return nullptr; } auto *py_edges_it = PyObject_New(PyEdgesIterator, &PyEdgesIteratorType); @@ -1401,12 +1557,7 @@ PyObject *PyVertexIterProperties(PyVertex *self, PyObject *Py_UNUSED(ignored)) { MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); mgp_properties_iterator *properties_it{nullptr}; - if (const auto err = mgp_vertex_iter_properties(self->vertex, self->py_graph->memory, &properties_it); - err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_properties_iterator."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting mgp_properties_iterator."); + if (RaiseExceptionFromErrorCode(mgp_vertex_iter_properties(self->vertex, self->py_graph->memory, &properties_it))) { return nullptr; } auto *py_properties_it = PyObject_New(PyPropertiesIterator, &PyPropertiesIteratorType); @@ -1425,17 +1576,13 @@ PyObject *PyVertexGetProperty(PyVertex *self, PyObject *args) { MG_ASSERT(self->vertex); MG_ASSERT(self->py_graph); MG_ASSERT(self->py_graph->graph); - const char *prop_name = nullptr; + const char *prop_name{nullptr}; if (!PyArg_ParseTuple(args, "s", &prop_name)) { return nullptr; } mgp_value *prop_value{nullptr}; - if (const auto err = mgp_vertex_get_property(self->vertex, prop_name, self->py_graph->memory, &prop_value); - err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_value for vertex property value."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting mgp_vertex property."); + if (RaiseExceptionFromErrorCode( + mgp_vertex_get_property(self->vertex, prop_name, self->py_graph->memory, &prop_value))) { return nullptr; } auto py_prop_value = MgpValueToPyObject(*prop_value, self->py_graph); @@ -1443,15 +1590,70 @@ PyObject *PyVertexGetProperty(PyVertex *self, PyObject *args) { return py_prop_value.Steal(); } +PyObject *PyVertexSetProperty(PyVertex *self, PyObject *args) { + MG_ASSERT(self); + MG_ASSERT(self->vertex); + MG_ASSERT(self->py_graph); + MG_ASSERT(self->py_graph->graph); + const char *prop_name = nullptr; + PyObject *py_value{nullptr}; + if (!PyArg_ParseTuple(args, "sO", &prop_name, &py_value)) { + return nullptr; + } + MgpUniquePtr prop_value{PyObjectToMgpValueWithPythonExceptions(py_value, self->py_graph->memory), + mgp_value_destroy}; + + if (prop_value == nullptr || + RaiseExceptionFromErrorCode(mgp_vertex_set_property(self->vertex, prop_name, prop_value.get()))) { + return nullptr; + } + Py_RETURN_NONE; +} + +PyObject *PyVertexAddLabel(PyVertex *self, PyObject *args) { + MG_ASSERT(self); + MG_ASSERT(self->vertex); + MG_ASSERT(self->py_graph); + MG_ASSERT(self->py_graph->graph); + const char *label_name = nullptr; + if (!PyArg_ParseTuple(args, "s", &label_name)) { + return nullptr; + } + if (RaiseExceptionFromErrorCode(mgp_vertex_add_label(self->vertex, mgp_label{label_name}))) { + return nullptr; + } + Py_RETURN_NONE; +} + +PyObject *PyVertexRemoveLabel(PyVertex *self, PyObject *args) { + MG_ASSERT(self); + MG_ASSERT(self->vertex); + MG_ASSERT(self->py_graph); + MG_ASSERT(self->py_graph->graph); + const char *label_name = nullptr; + if (!PyArg_ParseTuple(args, "s", &label_name)) { + return nullptr; + } + if (RaiseExceptionFromErrorCode(mgp_vertex_remove_label(self->vertex, mgp_label{label_name}))) { + return nullptr; + } + Py_RETURN_NONE; +} + static PyMethodDef PyVertexMethods[] = { {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported."}, {"is_valid", reinterpret_cast(PyVertexIsValid), METH_NOARGS, - "Return True if Vertex is in valid context and may be used."}, + "Return True if the vertex is in valid context and may be used."}, + {"underlying_graph_is_mutable", reinterpret_cast(PyVertexUnderlyingGraphIsMutable), METH_NOARGS, + "Return True if the vertex is mutable and can be modified."}, {"get_id", reinterpret_cast(PyVertexGetId), METH_NOARGS, "Return vertex id."}, {"labels_count", reinterpret_cast(PyVertexLabelsCount), METH_NOARGS, "Return number of lables of a vertex."}, {"label_at", reinterpret_cast(PyVertexLabelAt), METH_VARARGS, "Return label of a vertex on a given index."}, + {"add_label", reinterpret_cast(PyVertexAddLabel), METH_VARARGS, "Add the label to the vertex."}, + {"remove_label", reinterpret_cast(PyVertexRemoveLabel), METH_VARARGS, + "Remove the label from the vertex."}, {"iter_in_edges", reinterpret_cast(PyVertexIterInEdges), METH_NOARGS, "Return _mgp.EdgesIterator for in edges."}, {"iter_out_edges", reinterpret_cast(PyVertexIterOutEdges), METH_NOARGS, @@ -1460,6 +1662,8 @@ static PyMethodDef PyVertexMethods[] = { "Return _mgp.PropertiesIterator for this vertex."}, {"get_property", reinterpret_cast(PyVertexGetProperty), METH_VARARGS, "Return vertex property with given name."}, + {"set_property", reinterpret_cast(PyVertexSetProperty), METH_VARARGS, + "Set the value of the property on the vertex."}, {nullptr}, }; @@ -1478,14 +1682,13 @@ static PyTypeObject PyVertexType = { }; // clang-format on -PyObject *MakePyVertex(mgp_vertex *vertex, PyGraph *py_graph) { - MG_ASSERT(vertex); +PyObject *MakePyVertexWithoutCopy(mgp_vertex &vertex, PyGraph *py_graph) { MG_ASSERT(py_graph); MG_ASSERT(py_graph->graph && py_graph->memory); - MG_ASSERT(vertex->GetMemoryResource() == py_graph->memory->impl); + MG_ASSERT(vertex.GetMemoryResource() == py_graph->memory->impl); auto *py_vertex = PyObject_New(PyVertex, &PyVertexType); if (!py_vertex) return nullptr; - py_vertex->vertex = vertex; + py_vertex->vertex = &vertex; py_vertex->py_graph = py_graph; Py_INCREF(py_graph); return reinterpret_cast(py_vertex); @@ -1495,16 +1698,14 @@ PyObject *MakePyVertex(mgp_vertex &vertex, PyGraph *py_graph) { MG_ASSERT(py_graph); MG_ASSERT(py_graph->graph && py_graph->memory); - mgp_vertex *vertex_copy{nullptr}; - if (const auto err = mgp_vertex_copy(&vertex, py_graph->memory, &vertex_copy); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_vertex."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during creating mgp_vertex"); + MgpUniquePtr vertex_copy{nullptr, mgp_vertex_destroy}; + if (RaiseExceptionFromErrorCode(CreateMgpObject(vertex_copy, mgp_vertex_copy, &vertex, py_graph->memory))) { return nullptr; } - auto *py_vertex = MakePyVertex(vertex_copy, py_graph); - if (!py_vertex) mgp_vertex_destroy(vertex_copy); + auto *py_vertex = MakePyVertexWithoutCopy(*vertex_copy, py_graph); + if (py_vertex != nullptr) { + static_cast(vertex_copy.release()); + } return py_vertex; } @@ -1521,12 +1722,7 @@ PyObject *PyVertexRichCompare(PyObject *self, PyObject *other, int op) { MG_ASSERT(v1->vertex); MG_ASSERT(v2->vertex); - int equals{0}; - if (const auto err = mgp_vertex_equal(v1->vertex, v2->vertex, &equals); err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during comparing vertices"); - return nullptr; - } - return PyBool_FromLong(equals); + return PyBool_FromLong(Call(mgp_vertex_equal, v1->vertex, v2->vertex)); } // clang-format off @@ -1563,11 +1759,8 @@ PyObject *PyPathExpand(PyPath *self, PyObject *edge) { return nullptr; } auto *py_edge = reinterpret_cast(edge); - if (const auto err = mgp_path_expand(self->path, py_edge->edge); err == MGP_ERROR_LOGIC_ERROR) { - PyErr_SetString(PyExc_ValueError, "Edge is not a continuation of the path."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_MemoryError, "Unable to expand mgp_path."); + + if (RaiseExceptionFromErrorCode(mgp_path_expand(self->path, py_edge->edge))) { return nullptr; } Py_RETURN_NONE; @@ -1590,11 +1783,7 @@ PyObject *PyPathVertexAt(PyPath *self, PyObject *args) { return nullptr; } mgp_vertex *vertex{nullptr}; - if (const auto err = mgp_path_vertex_at(self->path, i, &vertex); err == MGP_ERROR_OUT_OF_RANGE) { - PyErr_SetString(PyExc_IndexError, "Index is out of range."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting vertex from path."); + if (RaiseExceptionFromErrorCode(mgp_path_vertex_at(self->path, i, &vertex))) { return nullptr; } return MakePyVertex(*vertex, self->py_graph); @@ -1610,11 +1799,7 @@ PyObject *PyPathEdgeAt(PyPath *self, PyObject *args) { return nullptr; } mgp_edge *edge{nullptr}; - if (const auto err = mgp_path_edge_at(self->path, i, &edge); err == MGP_ERROR_OUT_OF_RANGE) { - PyErr_SetString(PyExc_IndexError, "Index is out of range."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during getting edge from path."); + if (RaiseExceptionFromErrorCode(mgp_path_edge_at(self->path, i, &edge))) { return nullptr; } return MakePyEdge(*edge, self->py_graph); @@ -1664,11 +1849,8 @@ PyObject *MakePyPath(mgp_path &path, PyGraph *py_graph) { MG_ASSERT(py_graph); MG_ASSERT(py_graph->graph && py_graph->memory); mgp_path *path_copy{nullptr}; - if (const auto err = mgp_path_copy(&path, py_graph->memory, &path_copy); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_path."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during copying a path"); + + if (RaiseExceptionFromErrorCode(mgp_path_copy(&path, py_graph->memory, &path_copy))) { return nullptr; } auto *py_path = MakePyPath(path_copy, py_graph); @@ -1687,12 +1869,7 @@ PyObject *PyPathMakeWithStart(PyTypeObject *type, PyObject *vertex) { } auto *py_vertex = reinterpret_cast(vertex); mgp_path *path{nullptr}; - if (const auto err = mgp_path_make_with_start(py_vertex->vertex, py_vertex->py_graph->memory, &path); - err == MGP_ERROR_UNABLE_TO_ALLOCATE) { - PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_path."); - return nullptr; - } else if (err != MGP_ERROR_NO_ERROR) { - PyErr_SetString(PyExc_RuntimeError, "Unexpected error during creating a path"); + if (RaiseExceptionFromErrorCode(mgp_path_make_with_start(py_vertex->vertex, py_vertex->py_graph->memory, &path))) { return nullptr; } auto *py_path = MakePyPath(path, py_vertex->py_graph); @@ -1700,6 +1877,13 @@ PyObject *PyPathMakeWithStart(PyTypeObject *type, PyObject *vertex) { return py_path; } +struct PyMgpError { + const char *name; + PyObject *&exception; + PyObject *&base; + const char *docstring; +}; + PyObject *PyInitMgpModule() { PyObject *mgp = PyModule_Create(&PyMgpModule); if (!mgp) return nullptr; @@ -1728,12 +1912,50 @@ PyObject *PyInitMgpModule() { if (!register_type(&PyCypherTypeType, "Type")) return nullptr; if (!register_type(&PyMessagesType, "Messages")) return nullptr; if (!register_type(&PyMessageType, "Message")) return nullptr; + + std::array py_mgp_errors{ + PyMgpError{"_mgp.UnknownError", gMgpUnknownError, PyExc_RuntimeError, nullptr}, + PyMgpError{"_mgp.UnableToAllocateError", gMgpUnableToAllocateError, PyExc_MemoryError, nullptr}, + PyMgpError{"_mgp.InsufficientBufferError", gMgpInsufficientBufferError, PyExc_BufferError, nullptr}, + PyMgpError{"_mgp.OutOfRangeError", gMgpOutOfRangeError, PyExc_BufferError, nullptr}, + PyMgpError{"_mgp.LogicErrorError", gMgpLogicErrorError, PyExc_RuntimeError, nullptr}, + PyMgpError{"_mgp.DeletedObjectError", gMgpDeletedObjectError, PyExc_RuntimeError, nullptr}, + PyMgpError{"_mgp.InvalidArgumentError", gMgpInvalidArgumentError, PyExc_ValueError, nullptr}, + PyMgpError{"_mgp.KeyAlreadyExistsError", gMgpKeyAlreadyExistsError, PyExc_RuntimeError, nullptr}, + PyMgpError{"_mgp.ImmutableObjectError", gMgpImmutableObjectError, PyExc_RuntimeError, nullptr}, + PyMgpError{"_mgp.ValueConversionError", gMgpValueConversionError, PyExc_RuntimeError, nullptr}, + PyMgpError{"_mgp.SerializationError", gMgpSerializationError, PyExc_RuntimeError, nullptr}, + }; Py_INCREF(Py_None); - if (PyModule_AddObject(mgp, "_MODULE", Py_None) < 0) { + + utils::OnScopeExit clean_up{[mgp, &py_mgp_errors] { + for (const auto &py_mgp_error : py_mgp_errors) { + Py_XDECREF(py_mgp_error.exception); + } Py_DECREF(Py_None); Py_DECREF(mgp); + }}; + + if (PyModule_AddObject(mgp, "_MODULE", Py_None) < 0) { return nullptr; } + + auto register_custom_error = [mgp](PyMgpError &py_mgp_error) { + py_mgp_error.exception = PyErr_NewException(py_mgp_error.name, py_mgp_error.base, nullptr); + if (py_mgp_error.exception == nullptr) { + return false; + } + + const auto *name_in_module = std::string_view(py_mgp_error.name).substr(5).data(); + return PyModule_AddObject(mgp, name_in_module, py_mgp_error.exception) == 0; + }; + + for (auto &py_mgp_error : py_mgp_errors) { + if (!register_custom_error(py_mgp_error)) { + return nullptr; + } + } + clean_up.Disable(); return mgp; } @@ -1840,8 +2062,7 @@ mgp_value *PyObjectToMgpValue(PyObject *o, mgp_memory *memory) { auto py_seq_to_list = [memory](PyObject *seq, Py_ssize_t len, const auto &py_seq_get_item) { static_assert(std::numeric_limits::max() <= std::numeric_limits::max()); MgpUniquePtr list{nullptr, &mgp_list_destroy}; - if (const auto err = CreateMgpObject(list, mgp_list_make_empty, len, memory); - err == MGP_ERROR_UNABLE_TO_ALLOCATE) { + if (const auto err = CreateMgpObject(list, mgp_list_make_empty, len, memory); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { throw std::bad_alloc{}; } else if (err != MGP_ERROR_NO_ERROR) { throw std::runtime_error{"Unexpected error during making mgp_list"}; @@ -2040,4 +2261,64 @@ mgp_value *PyObjectToMgpValue(PyObject *o, mgp_memory *memory) { return mgp_v; } +PyObject *PyGraphCreateEdge(PyGraph *self, PyObject *args) { + MG_ASSERT(PyGraphIsValidImpl(*self)); + MG_ASSERT(self->memory); + PyVertex *from{nullptr}; + PyVertex *to{nullptr}; + const char *edge_type{nullptr}; + if (!PyArg_ParseTuple(args, "O!O!s", &PyVertexType, &from, &PyVertexType, &to, &edge_type)) { + return nullptr; + } + MgpUniquePtr new_edge{nullptr, mgp_edge_destroy}; + if (RaiseExceptionFromErrorCode(CreateMgpObject(new_edge, mgp_graph_create_edge, self->graph, from->vertex, + to->vertex, mgp_edge_type{edge_type}, self->memory))) { + return nullptr; + } + auto *py_edge = MakePyEdgeWithoutCopy(*new_edge, self); + if (py_edge != nullptr) { + static_cast(new_edge.release()); + } + return py_edge; +} + +PyObject *PyGraphDeleteVertex(PyGraph *self, PyObject *args) { + MG_ASSERT(PyGraphIsValidImpl(*self)); + MG_ASSERT(self->memory); + PyVertex *vertex{nullptr}; + if (!PyArg_ParseTuple(args, "O!", &PyVertexType, &vertex)) { + return nullptr; + } + if (RaiseExceptionFromErrorCode(mgp_graph_delete_vertex(self->graph, vertex->vertex))) { + return nullptr; + } + Py_RETURN_NONE; +} + +PyObject *PyGraphDetachDeleteVertex(PyGraph *self, PyObject *args) { + MG_ASSERT(PyGraphIsValidImpl(*self)); + MG_ASSERT(self->memory); + PyVertex *vertex{nullptr}; + if (!PyArg_ParseTuple(args, "O!", &PyVertexType, &vertex)) { + return nullptr; + } + if (RaiseExceptionFromErrorCode(mgp_graph_detach_delete_vertex(self->graph, vertex->vertex))) { + return nullptr; + } + Py_RETURN_NONE; +} + +PyObject *PyGraphDeleteEdge(PyGraph *self, PyObject *args) { + MG_ASSERT(PyGraphIsValidImpl(*self)); + MG_ASSERT(self->memory); + PyEdge *edge{nullptr}; + if (!PyArg_ParseTuple(args, "O!", &PyEdgeType, &edge)) { + return nullptr; + } + if (RaiseExceptionFromErrorCode(mgp_graph_delete_edge(self->graph, edge->edge))) { + return nullptr; + } + Py_RETURN_NONE; +} + } // namespace query::procedure diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index 4b2a11a9d..cbdc030af 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -1,5 +1,16 @@ +function(copy_e2e_python_files TARGET_PREFIX FILE_NAME) +add_custom_target(memgraph__e2e__${TARGET_PREFIX}__${FILE_NAME} ALL + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME} + ${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME}) +endfunction() + add_subdirectory(replication) add_subdirectory(memory) add_subdirectory(triggers) add_subdirectory(isolation_levels) add_subdirectory(streams) +add_subdirectory(write_procedures) + +copy_e2e_python_files(pytest_runner pytest_runner.sh "") diff --git a/tests/e2e/streams/streams_test_runner.sh b/tests/e2e/pytest_runner.sh similarity index 100% rename from tests/e2e/streams/streams_test_runner.sh rename to tests/e2e/pytest_runner.sh diff --git a/tests/e2e/streams/CMakeLists.txt b/tests/e2e/streams/CMakeLists.txt index 44d5506f4..d73de22d2 100644 --- a/tests/e2e/streams/CMakeLists.txt +++ b/tests/e2e/streams/CMakeLists.txt @@ -1,14 +1,10 @@ function(copy_streams_e2e_python_files FILE_NAME) -add_custom_target(memgraph__e2e__streams__${FILE_NAME} ALL - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME} - ${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME} - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME}) + copy_e2e_python_files(streams ${FILE_NAME}) endfunction() copy_streams_e2e_python_files(common.py) copy_streams_e2e_python_files(conftest.py) copy_streams_e2e_python_files(streams_tests.py) copy_streams_e2e_python_files(streams_owner_tests.py) -copy_streams_e2e_python_files(streams_test_runner.sh) + add_subdirectory(transformations) diff --git a/tests/e2e/streams/workloads.yaml b/tests/e2e/streams/workloads.yaml index f9f52f9f3..7c60f64c7 100644 --- a/tests/e2e/streams/workloads.yaml +++ b/tests/e2e/streams/workloads.yaml @@ -8,12 +8,12 @@ template_cluster: &template_cluster workloads: - name: "Streams start, stop and show" - binary: "tests/e2e/streams/streams_test_runner.sh" + binary: "tests/e2e/pytest_runner.sh" proc: "tests/e2e/streams/transformations/" - args: ["streams_tests.py"] + args: ["streams/streams_tests.py"] <<: *template_cluster - name: "Streams with users" - binary: "tests/e2e/streams/streams_test_runner.sh" + binary: "tests/e2e/pytest_runner.sh" proc: "tests/e2e/streams/transformations/" - args: ["streams_owner_tests.py"] + args: ["streams/streams_owner_tests.py"] <<: *template_cluster diff --git a/tests/e2e/write_procedures/CMakeLists.txt b/tests/e2e/write_procedures/CMakeLists.txt new file mode 100644 index 000000000..72b640271 --- /dev/null +++ b/tests/e2e/write_procedures/CMakeLists.txt @@ -0,0 +1,9 @@ +function(copy_write_procedures_e2e_python_files FILE_NAME) + copy_e2e_python_files(write_procedures ${FILE_NAME}) +endfunction() + +copy_write_procedures_e2e_python_files(common.py) +copy_write_procedures_e2e_python_files(conftest.py) +copy_write_procedures_e2e_python_files(simple_write.py) + +add_subdirectory(procedures) diff --git a/tests/e2e/write_procedures/common.py b/tests/e2e/write_procedures/common.py new file mode 100644 index 000000000..8cc9fe1af --- /dev/null +++ b/tests/e2e/write_procedures/common.py @@ -0,0 +1,23 @@ +import mgclient +import typing + + +def execute_and_fetch_all(cursor: mgclient.Cursor, query: str, + params: dict = {}) -> typing.List[tuple]: + cursor.execute(query, params) + return cursor.fetchall() + + +def connect(**kwargs) -> mgclient.Connection: + connection = mgclient.connect(host="localhost", port=7687, **kwargs) + connection.autocommit = True + return connection + + +def has_n_result_row(cursor: mgclient.Cursor, query: str, n: int): + results = execute_and_fetch_all(cursor, query) + return len(results) == n + + +def has_one_result_row(cursor: mgclient.Cursor, query: str): + return has_n_result_row(cursor, query, 1) diff --git a/tests/e2e/write_procedures/conftest.py b/tests/e2e/write_procedures/conftest.py new file mode 100644 index 000000000..2791edff3 --- /dev/null +++ b/tests/e2e/write_procedures/conftest.py @@ -0,0 +1,11 @@ +import pytest + +from common import execute_and_fetch_all, connect + + +@pytest.fixture(autouse=True) +def connection(): + connection = connect() + yield connection + cursor = connection.cursor() + execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n") diff --git a/tests/e2e/write_procedures/procedures/CMakeLists.txt b/tests/e2e/write_procedures/procedures/CMakeLists.txt new file mode 100644 index 000000000..279ff0bf5 --- /dev/null +++ b/tests/e2e/write_procedures/procedures/CMakeLists.txt @@ -0,0 +1,2 @@ +copy_write_procedures_e2e_python_files(write.py) +copy_write_procedures_e2e_python_files(read.py) diff --git a/tests/e2e/write_procedures/procedures/read.py b/tests/e2e/write_procedures/procedures/read.py new file mode 100644 index 000000000..ef4778e91 --- /dev/null +++ b/tests/e2e/write_procedures/procedures/read.py @@ -0,0 +1,12 @@ +import mgp + + +@mgp.read_proc +def underlying_graph_is_mutable(ctx: mgp.ProcCtx, + object: mgp.Any) -> mgp.Record(mutable=bool): + return mgp.Record(mutable=object.underlying_graph_is_mutable()) + + +@mgp.read_proc +def graph_is_mutable(ctx: mgp.ProcCtx) -> mgp.Record(mutable=bool): + return mgp.Record(mutable=ctx.graph.is_mutable()) diff --git a/tests/e2e/write_procedures/procedures/write.py b/tests/e2e/write_procedures/procedures/write.py new file mode 100644 index 000000000..9437db262 --- /dev/null +++ b/tests/e2e/write_procedures/procedures/write.py @@ -0,0 +1,74 @@ +import mgp + + +@mgp.write_proc +def create_vertex(ctx: mgp.ProcCtx) -> mgp.Record(v=mgp.Any): + v = None + try: + v = ctx.graph.create_vertex() + except RuntimeError as e: + return mgp.Record(v=str(e)) + return mgp.Record(v=v) + + +@mgp.write_proc +def delete_vertex(ctx: mgp.ProcCtx, v: mgp.Any) -> mgp.Record(): + ctx.graph.delete_vertex(v) + return mgp.Record() + + +@mgp.write_proc +def detach_delete_vertex(ctx: mgp.ProcCtx, v: mgp.Any) -> mgp.Record(): + ctx.graph.detach_delete_vertex(v) + return mgp.Record() + + +@mgp.write_proc +def create_edge(ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, + to_vertex: mgp.Vertex, + edge_type: str) -> mgp.Record(e=mgp.Any): + e = None + try: + e = ctx.graph.create_edge( + from_vertex, to_vertex, mgp.EdgeType(edge_type)) + except RuntimeError as ex: + return mgp.Record(e=str(ex)) + return mgp.Record(e=e) + + +@mgp.write_proc +def delete_edge(ctx: mgp.ProcCtx, edge: mgp.Edge) -> mgp.Record(): + ctx.graph.delete_edge(edge) + return mgp.Record() + + +@mgp.write_proc +def set_property(ctx: mgp.ProcCtx, object: mgp.Any, + name: str, value: mgp.Nullable[mgp.Any]) -> mgp.Record(): + object.properties.set(name, value) + return mgp.Record() + + +@mgp.write_proc +def add_label(ctx: mgp.ProcCtx, object: mgp.Any, + name: str) -> mgp.Record(o=mgp.Any): + object.add_label(name) + return mgp.Record(o=object) + + +@mgp.write_proc +def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, + name: str) -> mgp.Record(o=mgp.Any): + object.remove_label(name) + return mgp.Record(o=object) + + +@mgp.write_proc +def underlying_graph_is_mutable(ctx: mgp.ProcCtx, + object: mgp.Any) -> mgp.Record(mutable=bool): + return mgp.Record(mutable=object.underlying_graph_is_mutable()) + + +@mgp.write_proc +def graph_is_mutable(ctx: mgp.ProcCtx) -> mgp.Record(mutable=bool): + return mgp.Record(mutable=ctx.graph.is_mutable()) diff --git a/tests/e2e/write_procedures/simple_write.py b/tests/e2e/write_procedures/simple_write.py new file mode 100644 index 000000000..a6c5efea9 --- /dev/null +++ b/tests/e2e/write_procedures/simple_write.py @@ -0,0 +1,182 @@ +import typing +import mgclient +import sys +import pytest +from common import (execute_and_fetch_all, + has_one_result_row, has_n_result_row) + + +def test_is_write(connection): + is_write = 2 + result_order = "name, signature, is_write" + cursor = connection.cursor() + for proc in execute_and_fetch_all( + cursor, "CALL mg.procedures() YIELD * WITH name, signature, " + "is_write WHERE name STARTS WITH 'write' " + f"RETURN {result_order}"): + assert proc[is_write] is True + + for proc in execute_and_fetch_all( + cursor, "CALL mg.procedures() YIELD * WITH name, signature, " + "is_write WHERE NOT name STARTS WITH 'write' " + f"RETURN {result_order}"): + assert proc[is_write] is False + + assert cursor.description[0].name == "name" + assert cursor.description[1].name == "signature" + assert cursor.description[2].name == "is_write" + + +def test_single_vertex(connection): + cursor = connection.cursor() + assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) + result = execute_and_fetch_all( + cursor, "CALL write.create_vertex() YIELD v RETURN v") + vertex = result[0][0] + assert isinstance(vertex, mgclient.Node) + assert has_one_result_row(cursor, "MATCH (n) RETURN n") + assert vertex.labels == set() + assert vertex.properties == {} + + def add_label(label: str): + execute_and_fetch_all( + cursor, f"MATCH (n) CALL write.add_label(n, '{label}') " + "YIELD * RETURN *") + + def remove_label(label: str): + execute_and_fetch_all( + cursor, f"MATCH (n) CALL write.remove_label(n, '{label}') " + "YIELD * RETURN *") + + def get_vertex() -> mgclient.Node: + return execute_and_fetch_all(cursor, "MATCH (n) RETURN n")[0][0] + + def set_property(property_name: str, property: typing.Any): + nonlocal cursor + execute_and_fetch_all( + cursor, f"MATCH (n) CALL write.set_property(n, '{property_name}', " + "$property) YIELD * RETURN *", {"property": property}) + + label_1 = "LABEL1" + label_2 = "LABEL2" + + add_label(label_1) + assert get_vertex().labels == {label_1} + add_label(label_1) + assert get_vertex().labels == {label_1} + add_label(label_2) + assert get_vertex().labels == {label_1, label_2} + remove_label(label_1) + assert get_vertex().labels == {label_2} + property_name = "prop" + property_value_1 = 1 + property_value_2 = [42, 24] + set_property(property_name, property_value_1) + assert get_vertex().properties == {property_name: property_value_1} + set_property(property_name, property_value_2) + assert get_vertex().properties == {property_name: property_value_2} + set_property(property_name, None) + assert get_vertex().properties == {} + + execute_and_fetch_all( + cursor, "MATCH (n) CALL write.delete_vertex(n) YIELD * RETURN 1") + assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) + + +def test_single_edge(connection): + cursor = connection.cursor() + assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) + v1_id = execute_and_fetch_all( + cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id + v2_id = execute_and_fetch_all( + cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id + edge_type = "EDGE" + edge = execute_and_fetch_all( + cursor, f"MATCH (n) WHERE id(n) = {v1_id} " + f"MATCH (m) WHERE id(m) = {v2_id} " + f"CALL write.create_edge(n, m, '{edge_type}') " + "YIELD e RETURN e")[0][0] + + assert edge.type == edge_type + assert edge.properties == {} + property_name = "very_looong_prooooperty_naaame" + property_value_1 = {"a": 1, "b": 3.4, "c": [666]} + property_value_2 = 64 + + def get_edge() -> mgclient.Node: + return execute_and_fetch_all(cursor, "MATCH ()-[e]->() RETURN e")[0][0] + + def set_property(property_name: str, property: typing.Any): + nonlocal cursor + execute_and_fetch_all( + cursor, "MATCH ()-[e]->() " + f"CALL write.set_property(e, '{property_name}', " + "$property) YIELD * RETURN *", {"property": property}) + + set_property(property_name, property_value_1) + assert get_edge().properties == {property_name: property_value_1} + set_property(property_name, property_value_2) + assert get_edge().properties == {property_name: property_value_2} + set_property(property_name, None) + assert get_edge().properties == {} + execute_and_fetch_all( + cursor, "MATCH ()-[e]->() CALL write.delete_edge(e) YIELD * RETURN 1") + assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0) + + +def test_detach_delete_vertex(connection): + cursor = connection.cursor() + assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) + v1_id = execute_and_fetch_all( + cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id + v2_id = execute_and_fetch_all( + cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id + execute_and_fetch_all( + cursor, f"MATCH (n) WHERE id(n) = {v1_id} " + f"MATCH (m) WHERE id(m) = {v2_id} " + f"CALL write.create_edge(n, m, 'EDGE') " + "YIELD e RETURN e") + + assert has_one_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m") + execute_and_fetch_all( + cursor, f"MATCH (n) WHERE id(n) = {v1_id} " + "CALL write.detach_delete_vertex(n) YIELD * RETURN 1") + assert has_n_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m", 0) + assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0) + assert has_one_result_row( + cursor, f"MATCH (n) WHERE id(n) = {v2_id} RETURN n") + + +def test_graph_mutability(connection): + cursor = connection.cursor() + assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0) + v1_id = execute_and_fetch_all( + cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id + v2_id = execute_and_fetch_all( + cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id + execute_and_fetch_all( + cursor, f"MATCH (n) WHERE id(n) = {v1_id} " + f"MATCH (m) WHERE id(m) = {v2_id} " + f"CALL write.create_edge(n, m, 'EDGE') " + "YIELD e RETURN e") + + def test_mutability(is_write: bool): + module = "write" if is_write else "read" + assert execute_and_fetch_all( + cursor, f"CALL {module}.graph_is_mutable() " + "YIELD mutable RETURN mutable")[0][0] is is_write + assert execute_and_fetch_all( + cursor, "MATCH (n) " + f"CALL {module}.underlying_graph_is_mutable(n) " + "YIELD mutable RETURN mutable")[0][0] is is_write + assert execute_and_fetch_all( + cursor, "MATCH (n)-[e]->(m) " + f"CALL {module}.underlying_graph_is_mutable(e) " + "YIELD mutable RETURN mutable")[0][0] is is_write + + test_mutability(True) + test_mutability(False) + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/write_procedures/workloads.yaml b/tests/e2e/write_procedures/workloads.yaml new file mode 100644 index 000000000..a6a57231f --- /dev/null +++ b/tests/e2e/write_procedures/workloads.yaml @@ -0,0 +1,14 @@ +template_cluster: &template_cluster + cluster: + main: + args: ["--bolt-port", "7687", "--log-level=TRACE"] + log_file: "write-procedures-e2e.log" + setup_queries: [] + validation_queries: [] + +workloads: + - name: "Write procedures simple" + binary: "tests/e2e/pytest_runner.sh" + proc: "tests/e2e/write_procedures/procedures/" + args: ["write_procedures/simple_write.py"] + <<: *template_cluster diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index 06282eb8d..ed3202a7f 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -2698,8 +2698,7 @@ TEST_P(CypherMainVisitorTest, CallYieldAsterisk) { ASSERT_TRUE(identifier->user_declared_); identifier_names.push_back(identifier->name_); } - std::vector expected_names{"name", "signature"}; - ASSERT_EQ(identifier_names, expected_names); + ASSERT_THAT(identifier_names, UnorderedElementsAre("name", "signature", "is_write")); ASSERT_EQ(identifier_names, call_proc->result_fields_); CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); } @@ -2724,8 +2723,7 @@ TEST_P(CypherMainVisitorTest, CallYieldAsteriskReturnAsterisk) { ASSERT_TRUE(identifier->user_declared_); identifier_names.push_back(identifier->name_); } - std::vector expected_names{"name", "signature"}; - ASSERT_EQ(identifier_names, expected_names); + ASSERT_THAT(identifier_names, UnorderedElementsAre("name", "signature", "is_write")); ASSERT_EQ(identifier_names, call_proc->result_fields_); CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); } diff --git a/tests/unit/query_procedures_mgp_graph.cpp b/tests/unit/query_procedures_mgp_graph.cpp index 0462a76bb..38a1f68b6 100644 --- a/tests/unit/query_procedures_mgp_graph.cpp +++ b/tests/unit/query_procedures_mgp_graph.cpp @@ -154,7 +154,7 @@ TEST_F(MgpGraphTest, CreateVertex) { read_uncommited_accessor.FindVertex(storage::Gid::FromInt(vertex_id.as_int), storage::View::NEW).has_value()); } -TEST_F(MgpGraphTest, RemoveVertex) { +TEST_F(MgpGraphTest, DeleteVertex) { storage::Gid vertex_id{}; { auto accessor = CreateDbAccessor(storage::IsolationLevel::SNAPSHOT_ISOLATION); @@ -168,11 +168,24 @@ TEST_F(MgpGraphTest, RemoveVertex) { MgpVertexPtr vertex{ EXPECT_MGP_NO_ERROR(mgp_vertex *, mgp_graph_get_vertex_by_id, &graph, mgp_vertex_id{vertex_id.AsInt()}, &memory)}; EXPECT_NE(vertex, nullptr); - EXPECT_SUCCESS(mgp_graph_remove_vertex(&graph, vertex.get())); + EXPECT_SUCCESS(mgp_graph_delete_vertex(&graph, vertex.get())); EXPECT_EQ(CountVertices(read_uncommited_accessor, storage::View::NEW), 0); } -TEST_F(MgpGraphTest, CreateRemoveWithImmutableGraph) { +TEST_F(MgpGraphTest, DetachDeleteVertex) { + const auto vertex_ids = CreateEdge(); + auto graph = CreateGraph(); + auto read_uncommited_accessor = storage.Access(storage::IsolationLevel::READ_UNCOMMITTED); + EXPECT_EQ(CountVertices(read_uncommited_accessor, storage::View::NEW), 2); + MgpVertexPtr vertex{EXPECT_MGP_NO_ERROR(mgp_vertex *, mgp_graph_get_vertex_by_id, &graph, + mgp_vertex_id{vertex_ids.front().AsInt()}, &memory)}; + EXPECT_EQ(mgp_graph_delete_vertex(&graph, vertex.get()), MGP_ERROR_LOGIC_ERROR); + EXPECT_EQ(CountVertices(read_uncommited_accessor, storage::View::NEW), 2); + EXPECT_SUCCESS(mgp_graph_detach_delete_vertex(&graph, vertex.get())); + EXPECT_EQ(CountVertices(read_uncommited_accessor, storage::View::NEW), 1); +} + +TEST_F(MgpGraphTest, CreateDeleteWithImmutableGraph) { storage::Gid vertex_id{}; { auto accessor = CreateDbAccessor(storage::IsolationLevel::SNAPSHOT_ISOLATION); @@ -189,10 +202,10 @@ TEST_F(MgpGraphTest, CreateRemoveWithImmutableGraph) { MgpVertexPtr created_vertex{raw_vertex}; EXPECT_EQ(created_vertex, nullptr); EXPECT_EQ(CountVertices(read_uncommited_accessor, storage::View::NEW), 1); - MgpVertexPtr vertex_to_remove{EXPECT_MGP_NO_ERROR(mgp_vertex *, mgp_graph_get_vertex_by_id, &immutable_graph, + MgpVertexPtr vertex_to_delete{EXPECT_MGP_NO_ERROR(mgp_vertex *, mgp_graph_get_vertex_by_id, &immutable_graph, mgp_vertex_id{vertex_id.AsInt()}, &memory)}; - ASSERT_NE(vertex_to_remove, nullptr); - EXPECT_EQ(mgp_graph_remove_vertex(&immutable_graph, vertex_to_remove.get()), MGP_ERROR_IMMUTABLE_OBJECT); + ASSERT_NE(vertex_to_delete, nullptr); + EXPECT_EQ(mgp_graph_delete_vertex(&immutable_graph, vertex_to_delete.get()), MGP_ERROR_IMMUTABLE_OBJECT); EXPECT_EQ(CountVertices(read_uncommited_accessor, storage::View::NEW), 1); } @@ -376,7 +389,7 @@ TEST_F(MgpGraphTest, ModifyImmutableVertex) { EXPECT_EQ(mgp_vertex_set_property(vertex.get(), "property", value.get()), MGP_ERROR_IMMUTABLE_OBJECT); } -TEST_F(MgpGraphTest, CreateRemoveEdge) { +TEST_F(MgpGraphTest, CreateDeleteEdge) { std::array vertex_ids{}; { auto accessor = CreateDbAccessor(storage::IsolationLevel::SNAPSHOT_ISOLATION); @@ -397,11 +410,11 @@ TEST_F(MgpGraphTest, CreateRemoveEdge) { mgp_edge_type{"EDGE"}, &memory)}; CheckEdgeCountBetween(from, to, 1); ASSERT_NE(edge, nullptr); - EXPECT_SUCCESS(mgp_graph_remove_edge(&graph, edge.get())); + EXPECT_SUCCESS(mgp_graph_delete_edge(&graph, edge.get())); CheckEdgeCountBetween(from, to, 0); } -TEST_F(MgpGraphTest, CreateRemoveEdgeWithImmutableGraph) { +TEST_F(MgpGraphTest, CreateDeleteEdgeWithImmutableGraph) { storage::Gid from_id; storage::Gid to_id; { @@ -431,9 +444,9 @@ TEST_F(MgpGraphTest, CreateRemoveEdgeWithImmutableGraph) { EXPECT_MGP_NO_ERROR(mgp_edges_iterator *, mgp_vertex_iter_out_edges, from.get(), &memory)}; auto *edge_from_it = EXPECT_MGP_NO_ERROR(mgp_edge *, mgp_edges_iterator_get, edges_it.get()); ASSERT_NE(edge_from_it, nullptr); - EXPECT_EQ(mgp_graph_remove_edge(&graph, edge_from_it), MGP_ERROR_IMMUTABLE_OBJECT); + EXPECT_EQ(mgp_graph_delete_edge(&graph, edge_from_it), MGP_ERROR_IMMUTABLE_OBJECT); MgpEdgePtr edge_copy_of_immutable{EXPECT_MGP_NO_ERROR(mgp_edge *, mgp_edge_copy, edge_from_it, &memory)}; - EXPECT_EQ(mgp_graph_remove_edge(&graph, edge_copy_of_immutable.get()), MGP_ERROR_IMMUTABLE_OBJECT); + EXPECT_EQ(mgp_graph_delete_edge(&graph, edge_copy_of_immutable.get()), MGP_ERROR_IMMUTABLE_OBJECT); CheckEdgeCountBetween(from, to, 1); }