diff --git a/python/tests/graphql/test_nodes_property_filter.py b/python/tests/graphql/test_nodes_property_filter.py index b2054db77..af6e84402 100644 --- a/python/tests/graphql/test_nodes_property_filter.py +++ b/python/tests/graphql/test_nodes_property_filter.py @@ -128,7 +128,7 @@ def test_node_property_filter_equal_no_value_error(graph): run_graphql_error_test(query, expected_error_message, graph()) -@pytest.mark.parametrize("graph", [Graph]) +@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) def test_node_property_filter_equal_type_error(graph): query = """ query { diff --git a/python/tests/test_graphdb/test_graphdb_imports.py b/python/tests/test_graphdb/test_graphdb_imports.py index 0dd6fda55..793817d03 100644 --- a/python/tests/test_graphdb/test_graphdb_imports.py +++ b/python/tests/test_graphdb/test_graphdb_imports.py @@ -19,6 +19,13 @@ def test_import_into_graph(): assert res.properties.get("temp") == True assert res.properties.constant.get("con") == 11 + gg = Graph() + gg.add_node(1, "B") + with pytest.raises(Exception) as excinfo: + gg.import_nodes([g_a, g_b]) + assert "Nodes already exist" in str(excinfo.value) + assert gg.node("A") is None + gg = Graph() gg.import_nodes([g_a, g_b]) assert len(gg.nodes) == 2 @@ -35,11 +42,19 @@ def test_import_into_graph(): assert res.properties.as_dict() == props e_c_d = g.add_edge(4, "C", "D") + gg = Graph() gg.import_edges([e_a_b, e_c_d]) assert len(gg.nodes) == 4 assert len(gg.edges) == 2 + gg = Graph() + gg.add_edge(1, "C", "D") + with pytest.raises(Exception) as excinfo: + gg.import_edges([e_a_b, e_c_d]) + assert "Edges already exist" in str(excinfo.value) + assert gg.edge("A", "B") is None + def test_import_with_int(): g = Graph() @@ -58,6 +73,222 @@ def test_import_with_int(): assert g2.count_nodes() == g.count_nodes() +def test_import_node_as(): + g = Graph() + a = g.add_node(1, "A") + b = g.add_node(1, "B", {"temp": True}) + b.add_constant_properties({"con": 11}) + + gg = Graph() + res = gg.import_node_as(a, "X") + assert res.name == "X" + assert res.history().tolist() == [1] + + gg.add_node(1, "Y") + + with pytest.raises(Exception) as excinfo: + gg.import_node_as(b, "Y") + + assert "Node already exists" in str(excinfo.value) + + assert gg.nodes.name == ["X", "Y"] + y = gg.node("Y") + assert y.name == "Y" + assert y.history().tolist() == [1] + assert y.properties.get("temp") is None + assert y.properties.constant.get("con") is None + + +def test_import_node_as_merge(): + g = Graph() + a = g.add_node(1, "A") + b = g.add_node(1, "B", {"temp": True}) + b.add_constant_properties({"con": 11}) + + gg = Graph() + res = gg.import_node_as(a, "X") + assert res.name == "X" + assert res.history().tolist() == [1] + + gg.add_node(1, "Y") + gg.import_node_as(b, "Y", True) + + assert gg.nodes.name == ["X", "Y"] + y = gg.node("Y") + assert y.name == "Y" + assert y.history().tolist() == [1] + assert y.properties.get("temp") == True + assert y.properties.constant.get("con") == 11 + + +def test_import_nodes_as(): + g = Graph() + a = g.add_node(1, "A") + b = g.add_node(1, "B", {"temp": True}) + b.add_constant_properties({"con": 11}) + + gg = Graph() + gg.add_node(1, "Y") + + with pytest.raises(Exception) as excinfo: + gg.import_nodes_as([a, b], ["X", "Y"]) + + assert "Nodes already exist" in str(excinfo.value) + + assert gg.node("X") == None + + assert sorted(gg.nodes.name) == ["Y"] + y = gg.node("Y") + assert y.name == "Y" + assert y.history().tolist() == [1] + assert y.properties.get("temp") is None + assert y.properties.constant.get("con") is None + + +def test_import_nodes_as_merge(): + g = Graph() + a = g.add_node(1, "A") + b = g.add_node(1, "B", {"temp": True}) + b.add_constant_properties({"con": 11}) + + gg = Graph() + gg.add_node(1, "Y") + gg.import_nodes_as([a, b], ["X", "Y"], True) + + assert sorted(gg.nodes.name) == ["X", "Y"] + x = gg.node("X") + assert x.name == "X" + assert x.history().tolist() == [1] + + y = gg.node("Y") + assert y.name == "Y" + assert y.history().tolist() == [1] + assert y.properties.get("temp") == True + assert y.properties.constant.get("con") == 11 + + +def test_import_edge_as(): + g = Graph() + a = g.add_node(1, "A") + b = g.add_node(1, "B", {"temp": True}) + b.add_constant_properties({"con": 11}) + + e_a_b = g.add_edge(2, "A", "B", {"e_temp": True}) + e_b_c = g.add_edge(2, "B", "C", {"e_temp": True}) + + gg = Graph() + gg.add_edge(1, "X", "Y") + + gg.import_edge_as(e_b_c, ("Y", "Z")) + + with pytest.raises(Exception) as excinfo: + gg.import_edge_as(e_a_b, ("X", "Y")) + assert "Edge already exists" in str(excinfo.value) + + assert sorted(gg.nodes.name) == ["X", "Y", "Z"] + x = gg.node("X") + assert x.name == "X" + assert x.history().tolist() == [1] + + y = gg.node("Y") + assert y.name == "Y" + assert y.history().tolist() == [1, 2] + assert y.properties.get("temp") is None + assert y.properties.constant.get("con") is None + + e = gg.edge("X", "Y") + assert e.properties.get("e_temp") is None + + +def test_import_edge_as_merge(): + g = Graph() + a = g.add_node(1, "A") + b = g.add_node(1, "B", {"temp": True}) + b.add_constant_properties({"con": 11}) + + e_a_b = g.add_edge(2, "A", "B", {"e_temp": True}) + + gg = Graph() + gg.add_edge(3, "X", "Y") + gg.import_edge_as(e_a_b, ("X", "Y"), True) + + assert sorted(gg.nodes.name) == ["X", "Y"] + x = gg.node("X") + assert x.name == "X" + print(x.history()) + assert x.history().tolist() == [2, 3] + + y = gg.node("Y") + assert y.name == "Y" + assert y.history().tolist() == [2, 3] + assert y.properties.get("temp") is None + assert y.properties.constant.get("con") is None + + e = gg.edge("X", "Y") + assert e.properties.get("e_temp") == True + + +def test_import_edges_as(): + g = Graph() + a = g.add_node(1, "A") + b = g.add_node(1, "B", {"temp": True}) + b.add_constant_properties({"con": 11}) + c = g.add_node(1, "C") + + e_a_b = g.add_edge(2, "A", "B", {"e_temp": True}) + e_b_c = g.add_edge(2, "B", "C") + + gg = Graph() + gg.add_edge(1, "Y", "Z") + + with pytest.raises(Exception) as excinfo: + gg.import_edges_as([e_a_b, e_b_c], [("X", "Y"), ("Y", "Z")]) + assert "Edges already exist" in str(excinfo.value) + + assert sorted(gg.nodes.name) == ["Y", "Z"] + + y = gg.node("Y") + assert y.name == "Y" + assert y.history().tolist() == [1] + assert y.properties.get("temp") is None + assert y.properties.constant.get("con") is None + + z = gg.node("Z") + assert z.name == "Z" + assert z.history().tolist() == [1] + + +def test_import_edges_as_merge(): + g = Graph() + a = g.add_node(1, "A") + b = g.add_node(1, "B", {"temp": True}) + b.add_constant_properties({"con": 11}) + c = g.add_node(1, "C") + + e_a_b = g.add_edge(2, "A", "B", {"e_temp": True}) + e_b_c = g.add_edge(2, "B", "C") + + gg = Graph() + gg.add_edge(3, "Y", "Z") + gg.import_edges_as([e_a_b, e_b_c], [("X", "Y"), ("Y", "Z")], True) + + assert sorted(gg.nodes.name) == ["X", "Y", "Z"] + + x = gg.node("X") + assert x.name == "X" + assert x.history().tolist() == [2] + + y = gg.node("Y") + assert y.name == "Y" + assert y.history().tolist() == [2, 3] + assert y.properties.get("temp") is None + assert y.properties.constant.get("con") is None + + z = gg.node("Z") + assert z.name == "Z" + assert z.history().tolist() == [2, 3] + + def test_import_edges(): g = Graph() g.add_node(1, 1) diff --git a/raphtory/src/core/utils/errors.rs b/raphtory/src/core/utils/errors.rs index 3cb4ca3ee..dd3dd3238 100644 --- a/raphtory/src/core/utils/errors.rs +++ b/raphtory/src/core/utils/errors.rs @@ -137,9 +137,15 @@ pub enum GraphError { #[error("Node already exists with ID {0:?}")] NodeExistsError(GID), + #[error("Nodes already exist with IDs: {0:?}")] + NodesExistError(Vec), + #[error("Edge already exists for nodes {0:?} {1:?}")] EdgeExistsError(GID, GID), + #[error("Edges already exist with IDs: {0:?}")] + EdgesExistError(Vec<(GID, GID)>), + #[error("Node {0} does not exist")] NodeMissingError(GID), diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index 6d6f2a943..5608e9710 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -1,8 +1,12 @@ -use std::borrow::Borrow; +use raphtory_api::core::entities::{GID, VID}; +use std::{borrow::Borrow, fmt::Debug}; use crate::{ core::{ - entities::LayerIds, + entities::{ + nodes::node_ref::{AsNodeRef, NodeRef}, + LayerIds, + }, utils::errors::{ GraphError, GraphError::{EdgeExistsError, NodeExistsError}, @@ -32,14 +36,12 @@ pub trait ImportOps: { /// Imports a single node into the graph. /// - /// This function takes a reference to a node and an optional boolean flag `force`. - /// If `force` is `Some(false)` or `None`, the function will return an error if the node already exists in the graph. - /// If `force` is `Some(true)`, the function will overwrite the existing node in the graph. - /// /// # Arguments /// /// * `node` - A reference to the node to be imported. - /// * `force` - An optional boolean flag. If `Some(true)`, the function will overwrite the existing node. + /// * `merge` - An optional boolean flag. + /// If `merge` is `false`, the function will return an error if the imported node already exists in the graph. + /// If `merge` is `true`, the function merges the histories of the imported node and the existing node (in the graph). /// /// # Returns /// @@ -47,19 +49,42 @@ pub trait ImportOps: fn import_node<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, node: &NodeView, - force: bool, + merge: bool, ) -> Result, GraphError>; - /// Imports multiple nodes into the graph. + /// Imports a single node into the graph. /// - /// This function takes a vector of references to nodes and an optional boolean flag `force`. - /// If `force` is `Some(false)` or `None`, the function will return an error if any of the nodes already exist in the graph. - /// If `force` is `Some(true)`, the function will overwrite the existing nodes in the graph. + /// # Arguments + /// + /// * `node` - A reference to the node to be imported. + /// * `new_id` - The new node id. + /// * `merge` - An optional boolean flag. + /// If `merge` is `false`, the function will return an error if the imported node already exists in the graph. + /// If `merge` is `true`, the function merges the histories of the imported node and the existing node (in the graph). + /// + /// # Returns + /// + /// A `Result` which is `Ok` if the node was successfully imported, and `Err` otherwise. + fn import_node_as< + 'a, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, + >( + &self, + node: &NodeView, + new_id: V, + merge: bool, + ) -> Result, GraphError>; + + /// Imports multiple nodes into the graph. /// /// # Arguments /// /// * `nodes` - A vector of references to the nodes to be imported. - /// * `force` - An optional boolean flag. If `Some(true)`, the function will overwrite the existing nodes. + /// * `merge` - An optional boolean flag. + /// If `merge` is `false`, the function will return an error if any of the imported nodes already exists in the graph. + /// If `merge` is `true`, the function merges the histories of the imported nodes and the existing nodes (in the graph). /// /// # Returns /// @@ -67,19 +92,42 @@ pub trait ImportOps: fn import_nodes<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, nodes: impl IntoIterator>>, - force: bool, + merge: bool, ) -> Result<(), GraphError>; - /// Imports a single edge into the graph. + /// Imports multiple nodes into the graph. + /// + /// # Arguments + /// + /// * `nodes` - A vector of references to the nodes to be imported. + /// * `new_ids` - A list of node IDs to use for the imported nodes. + /// * `merge` - An optional boolean flag. + /// If `merge` is `false`, the function will return an error if any of the imported nodes already exists in the graph. + /// If `merge` is `true`, the function merges the histories of the imported nodes and the existing nodes (in the graph). + /// + /// # Returns /// - /// This function takes a reference to an edge and an optional boolean flag `force`. - /// If `force` is `Some(false)` or `None`, the function will return an error if the edge already exists in the graph. - /// If `force` is `Some(true)`, the function will overwrite the existing edge in the graph. + /// A `Result` which is `Ok` if the nodes were successfully imported, and `Err` otherwise. + fn import_nodes_as< + 'a, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, + >( + &self, + nodes: impl IntoIterator>>, + new_ids: impl IntoIterator, + merge: bool, + ) -> Result<(), GraphError>; + + /// Imports a single edge into the graph. /// /// # Arguments /// /// * `edge` - A reference to the edge to be imported. - /// * `force` - An optional boolean flag. If `Some(true)`, the function will overwrite the existing edge. + /// * `merge` - An optional boolean flag. + /// If `merge` is `false`, the function will return an error if the imported edge already exists in the graph. + /// If `merge` is `true`, the function merges the histories of the imported edge and the existing edge (in the graph). /// /// # Returns /// @@ -87,19 +135,42 @@ pub trait ImportOps: fn import_edge<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, edge: &EdgeView, - force: bool, + merge: bool, ) -> Result, GraphError>; - /// Imports multiple edges into the graph. + /// Imports a single edge into the graph. + /// + /// # Arguments /// - /// This function takes a vector of references to edges and an optional boolean flag `force`. - /// If `force` is `Some(false)` or `None`, the function will return an error if any of the edges already exist in the graph. - /// If `force` is `Some(true)`, the function will overwrite the existing edges in the graph. + /// * `edge` - A reference to the edge to be imported. + /// * `new_id` - The ID of the new edge. It's a tuple of the source and destination node ids. + /// * `merge` - An optional boolean flag. + /// If `merge` is `false`, the function will return an error if the imported edge already exists in the graph. + /// If `merge` is `true`, the function merges the histories of the imported edge and the existing edge (in the graph). + /// + /// # Returns + /// + /// A `Result` which is `Ok` if the edge was successfully imported, and `Err` otherwise. + fn import_edge_as< + 'a, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, + >( + &self, + edge: &EdgeView, + new_id: (V, V), + merge: bool, + ) -> Result, GraphError>; + + /// Imports multiple edges into the graph. /// /// # Arguments /// /// * `edges` - A vector of references to the edges to be imported. - /// * `force` - An optional boolean flag. If `Some(true)`, the function will overwrite the existing edges. + /// * `merge` - An optional boolean flag. + /// If `merge` is `false`, the function will return an error if any of the imported edges already exists in the graph. + /// If `merge` is `true`, the function merges the histories of the imported edges and the existing edges (in the graph). /// /// # Returns /// @@ -107,7 +178,32 @@ pub trait ImportOps: fn import_edges<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, edges: impl IntoIterator>>, - force: bool, + merge: bool, + ) -> Result<(), GraphError>; + + /// Imports multiple edges into the graph. + /// + /// # Arguments + /// + /// * `edges` - A vector of references to the edges to be imported. + /// * `new_ids` - The IDs of the new edges. It's a vector of tuples of the source and destination node ids. + /// * `merge` - An optional boolean flag. + /// If `merge` is `false`, the function will return an error if any of the imported edges already exists in the graph. + /// If `merge` is `true`, the function merges the histories of the imported edges and the existing edges (in the graph). + /// + /// # Returns + /// + /// A `Result` which is `Ok` if the edges were successfully imported, and `Err` otherwise. + fn import_edges_as< + 'a, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, + >( + &self, + edges: impl IntoIterator>>, + new_ids: impl IntoIterator, + merge: bool, ) -> Result<(), GraphError>; } @@ -122,56 +218,54 @@ impl< fn import_node<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, node: &NodeView, - force: bool, + merge: bool, ) -> Result, GraphError> { - if !force && self.node(node.id()).is_some() { - return Err(NodeExistsError(node.id())); - } - let node_internal = match node.node_type().as_str() { - None => self.resolve_node(node.id())?.inner(), - Some(node_type) => { - let (node_internal, _) = self.resolve_node_and_type(node.id(), node_type)?.inner(); - node_internal.inner() - } - }; - - for h in node.history() { - let t = time_from_input(self, h)?; - self.internal_add_node(t, node_internal, &[])?; - } - for (name, prop_view) in node.properties().temporal().iter() { - let old_prop_id = node - .graph - .node_meta() - .temporal_prop_meta() - .get_id(&name) - .unwrap(); - let dtype = node - .graph - .node_meta() - .temporal_prop_meta() - .get_dtype(old_prop_id) - .unwrap(); - let new_prop_id = self.resolve_node_property(&name, dtype, false)?.inner(); - for (h, prop) in prop_view.iter() { - let t = time_from_input(self, h)?; - self.internal_add_node(t, node_internal, &[(new_prop_id, prop)])?; - } - } - self.node(node.id()) - .expect("node added") - .add_constant_properties(node.properties().constant())?; + import_node_internal(&self, node, node.id(), merge) + } - Ok(self.node(node.id()).unwrap()) + fn import_node_as< + 'a, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, + >( + &self, + node: &NodeView, + new_id: V, + merge: bool, + ) -> Result, GraphError> { + import_node_internal(&self, node, new_id, merge) } fn import_nodes<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, nodes: impl IntoIterator>>, - force: bool, + merge: bool, ) -> Result<(), GraphError> { - for node in nodes { - self.import_node(node.borrow(), force)?; + let nodes: Vec<_> = nodes.into_iter().collect(); + let new_ids: Vec = nodes.iter().map(|n| n.borrow().id()).collect(); + check_existing_nodes(self, &new_ids, merge)?; + for node in &nodes { + self.import_node(node.borrow(), merge)?; + } + Ok(()) + } + + fn import_nodes_as< + 'a, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, + >( + &self, + nodes: impl IntoIterator>>, + new_ids: impl IntoIterator, + merge: bool, + ) -> Result<(), GraphError> { + let new_ids: Vec = new_ids.into_iter().collect(); + check_existing_nodes(self, &new_ids, merge)?; + for (node, new_node_id) in nodes.into_iter().zip(new_ids.into_iter()) { + self.import_node_as(node.borrow(), new_node_id, merge)?; } Ok(()) } @@ -179,62 +273,241 @@ impl< fn import_edge<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, edge: &EdgeView, - force: bool, + merge: bool, ) -> Result, GraphError> { - // make sure we preserve all layers even if they are empty - // skip default layer - for layer in edge.graph.unique_layers().skip(1) { - self.resolve_layer(Some(&layer))?; - } - if !force && self.has_edge(edge.src().id(), edge.dst().id()) { - return Err(EdgeExistsError(edge.src().id(), edge.dst().id())); - } - // Add edges first so we definitely have all associated nodes (important in case of persistent edges) - // FIXME: this needs to be verified - for ee in edge.explode_layers() { - let layer_id = ee.edge.layer().expect("exploded layers"); - let layer_ids = LayerIds::One(layer_id); - let layer_name = self.get_layer_name(layer_id); - let layer_name: Option<&str> = if layer_id == 0 { - None - } else { - Some(&layer_name) - }; - for ee in ee.explode() { - self.add_edge( - ee.time().expect("exploded edge"), - ee.src().id(), - ee.dst().id(), - ee.properties().temporal().collect_properties(), - layer_name, - )?; - } - - if self.include_deletions() { - for t in edge.graph.edge_deletion_history(edge.edge, &layer_ids) { - let ti = time_from_input(self, t.t())?; - let src_id = self.resolve_node(edge.src().id())?.inner(); - let dst_id = self.resolve_node(edge.dst().id())?.inner(); - let layer = self.resolve_layer(layer_name)?.inner(); - self.internal_delete_edge(ti, src_id, dst_id, layer)?; - } - } + import_edge_internal(&self, edge, edge.src().id(), edge.dst().id(), merge) + } - self.edge(ee.src().id(), ee.dst().id()) - .expect("edge added") - .add_constant_properties(ee.properties().constant(), layer_name)?; - } - Ok(self.edge(edge.src().id(), edge.dst().id()).unwrap()) + fn import_edge_as< + 'a, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, + >( + &self, + edge: &EdgeView, + new_id: (V, V), + merge: bool, + ) -> Result, GraphError> { + import_edge_internal(&self, edge, new_id.0, new_id.1, merge) } fn import_edges<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, edges: impl IntoIterator>>, - force: bool, + merge: bool, ) -> Result<(), GraphError> { + let edges: Vec<_> = edges.into_iter().collect(); + let new_ids: Vec<(GID, GID)> = edges.iter().map(|e| e.borrow().id()).collect(); + check_existing_edges(self, &new_ids, merge)?; for edge in edges { - self.import_edge(edge.borrow(), force)?; + self.import_edge(edge.borrow(), merge)?; + } + Ok(()) + } + + fn import_edges_as< + 'a, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, + >( + &self, + edges: impl IntoIterator>>, + new_ids: impl IntoIterator, + merge: bool, + ) -> Result<(), GraphError> { + let new_ids: Vec<(V, V)> = new_ids.into_iter().collect(); + check_existing_edges(self, &new_ids, merge)?; + for (new_id, edge) in new_ids.into_iter().zip(edges) { + self.import_edge_as(edge.borrow(), new_id, merge)?; } Ok(()) } } + +fn import_node_internal< + 'a, + G: StaticGraphViewOps + + InternalAdditionOps + + InternalDeletionOps + + InternalPropertyAdditionOps + + InternalMaterialize, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, +>( + graph: &G, + node: &NodeView, + id: V, + merge: bool, +) -> Result, GraphError> { + if !merge { + if let Some(existing_node) = graph.node(&id) { + return Err(NodeExistsError(existing_node.id())); + } + } + + let node_internal = match node.node_type().as_str() { + None => graph.resolve_node(&id)?.inner(), + Some(node_type) => { + let (node_internal, _) = graph.resolve_node_and_type(&id, node_type)?.inner(); + node_internal.inner() + } + }; + + for h in node.history() { + let t = time_from_input(graph, h)?; + graph.internal_add_node(t, node_internal, &[])?; + } + + for (name, prop_view) in node.properties().temporal().iter() { + let old_prop_id = node + .graph + .node_meta() + .temporal_prop_meta() + .get_id(&name) + .unwrap(); + let dtype = node + .graph + .node_meta() + .temporal_prop_meta() + .get_dtype(old_prop_id) + .unwrap(); + let new_prop_id = graph.resolve_node_property(&name, dtype, false)?.inner(); + for (h, prop) in prop_view.iter() { + let t = time_from_input(graph, h)?; + graph.internal_add_node(t, node_internal, &[(new_prop_id, prop)])?; + } + } + + graph + .node(&id) + .expect("node added") + .add_constant_properties(node.properties().constant())?; + + Ok(graph.node(&id).unwrap()) +} + +fn import_edge_internal< + 'a, + G: StaticGraphViewOps + + InternalAdditionOps + + InternalDeletionOps + + InternalPropertyAdditionOps + + InternalMaterialize, + GHH: GraphViewOps<'a>, + GH: GraphViewOps<'a>, + V: AsNodeRef + Clone + Debug, +>( + graph: &G, + edge: &EdgeView, + src_id: V, + dst_id: V, + merge: bool, +) -> Result, GraphError> { + // Preserve all layers even if they are empty (except the default layer) + for layer in edge.graph.unique_layers().skip(1) { + graph.resolve_layer(Some(&layer))?; + } + + if !merge && graph.has_edge(&src_id, &dst_id) { + if let Some(existing_edge) = graph.edge(&src_id, &dst_id) { + return Err(EdgeExistsError( + existing_edge.src().id(), + existing_edge.dst().id(), + )); + } + } + + // Add edges first to ensure associated nodes are present + for ee in edge.explode_layers() { + let layer_id = ee.edge.layer().expect("exploded layers"); + let layer_ids = LayerIds::One(layer_id); + let layer_name = graph.get_layer_name(layer_id); + let layer_name: Option<&str> = if layer_id == 0 { + None + } else { + Some(&layer_name) + }; + + for ee in ee.explode() { + graph.add_edge( + ee.time().expect("exploded edge"), + &src_id, + &dst_id, + ee.properties().temporal().collect_properties(), + layer_name, + )?; + } + + if graph.include_deletions() { + for t in edge.graph.edge_deletion_history(edge.edge, &layer_ids) { + let ti = time_from_input(graph, t.t())?; + let src_node = graph.resolve_node(&src_id)?.inner(); + let dst_node = graph.resolve_node(&dst_id)?.inner(); + let layer = graph.resolve_layer(layer_name)?.inner(); + graph.internal_delete_edge(ti, src_node, dst_node, layer)?; + } + } + + graph + .edge(&src_id, &dst_id) + .expect("edge added") + .add_constant_properties(ee.properties().constant(), layer_name)?; + } + + Ok(graph.edge(&src_id, &dst_id).unwrap()) +} + +fn check_existing_nodes< + G: StaticGraphViewOps + + InternalAdditionOps + + InternalDeletionOps + + InternalPropertyAdditionOps + + InternalMaterialize, + V: AsNodeRef, +>( + graph: &G, + ids: &[V], + merge: bool, +) -> Result<(), GraphError> { + if !merge { + let mut existing_nodes = vec![]; + for id in ids { + if let Some(node) = graph.node(id) { + existing_nodes.push(node.id()); + } + } + if !existing_nodes.is_empty() { + return Err(GraphError::NodesExistError(existing_nodes)); + } + } + Ok(()) +} + +fn check_existing_edges< + G: StaticGraphViewOps + + InternalAdditionOps + + InternalDeletionOps + + InternalPropertyAdditionOps + + InternalMaterialize, + V: AsNodeRef + Clone + Debug, +>( + graph: &G, + new_ids: &[(V, V)], + merge: bool, +) -> Result<(), GraphError> { + if !merge { + let mut existing_edges = vec![]; + for (src, dst) in new_ids { + if let Some(existing_edge) = graph.edge(src, dst) { + existing_edges.push((existing_edge.src().id(), existing_edge.dst().id())); + } + } + if !existing_edges.is_empty() { + return Err(GraphError::EdgesExistError(existing_edges)); + } + } + Ok(()) +} diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 3089a45a0..d7f74952f 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -417,7 +417,10 @@ mod db_tests { algorithms::components::weakly_connected_components, core::{ utils::{ - errors::GraphError, + errors::{ + GraphError, + GraphError::{EdgeExistsError, NodeExistsError, NodesExistError}, + }, time::{error::ParseTimeError, TryIntoTime}, }, Prop, @@ -629,6 +632,24 @@ mod db_tests { Prop::I64(11) ); + let gg = Graph::new(); + gg.add_node(1, "B", NO_PROPS, None).unwrap(); + let res = gg.import_nodes(vec![&g_a, &g_b], false); + match res { + Err(NodesExistError(ids)) => { + assert_eq!( + ids.into_iter() + .map(|id| id.to_string()) + .collect::>(), + vec!["B"], + ); + } + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(_) => panic!("Expected error but got Ok"), + } + + assert_eq!(gg.node("A"), None); + let gg = Graph::new(); let _ = gg.import_nodes(vec![&g_a, &g_b], false).unwrap(); assert_eq!(gg.nodes().name().collect_vec(), vec!["A", "B"]); @@ -654,9 +675,348 @@ mod db_tests { assert_eq!(res.properties().as_vec(), e_a_b_p.properties().as_vec()); let e_c_d = g.add_edge(4, "C", "D", NO_PROPS, None).unwrap(); + let gg = Graph::new(); let _ = gg.import_edges(vec![&e_a_b, &e_c_d], false).unwrap(); assert_eq!(gg.edges().len(), 2); + + let gg = Graph::new(); + let res = gg.add_edge(1, "C", "D", NO_PROPS, None); + let res = gg.import_edges(vec![&e_a_b, &e_c_d], false); + match res { + Err(GraphError::EdgesExistError(duplicates)) => { + assert_eq!( + duplicates + .into_iter() + .map(|(x, y)| (x.to_string(), y.to_string())) + .collect::>(), + vec![("C".to_string(), "D".to_string())] + ); + } + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(_) => panic!("Expected error but got Ok"), + } + assert_eq!(gg.edge("A", "B"), None); + } + + #[test] + fn import_node_as() { + let g = Graph::new(); + let g_a = g.add_node(0, "A", NO_PROPS, None).unwrap(); + let g_b = g + .add_node(1, "B", vec![("temp".to_string(), Prop::Bool(true))], None) + .unwrap(); + let _ = g_b.add_constant_properties(vec![("con".to_string(), Prop::I64(11))]); + + let gg = Graph::new(); + let res = gg.import_node_as(&g_a, "X", false).unwrap(); + assert_eq!(res.name(), "X"); + assert_eq!(res.history(), vec![0]); + + let _ = gg.add_node(1, "Y", NO_PROPS, None).unwrap(); + let res = gg.import_node_as(&g_b, "Y", false); + match res { + Err(NodeExistsError(id)) => { + assert_eq!(id.to_string(), "Y"); + } + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(_) => panic!("Expected error but got Ok"), + } + + let mut nodes = gg.nodes().name().collect_vec(); + nodes.sort(); + assert_eq!(nodes, vec!["X", "Y"]); // Nodes up until first failure are imported + let y = gg.node("Y").unwrap(); + + assert_eq!(y.name(), "Y"); + assert_eq!(y.history(), vec![1]); + assert_eq!(y.properties().get("temp"), None); + assert_eq!(y.properties().constant().get("con"), None); + } + + #[test] + fn import_node_as_merge() { + let g = Graph::new(); + let g_a = g.add_node(0, "A", NO_PROPS, None).unwrap(); + let g_b = g + .add_node(1, "B", vec![("temp".to_string(), Prop::Bool(true))], None) + .unwrap(); + let _ = g_b.add_constant_properties(vec![("con".to_string(), Prop::I64(11))]); + + let gg = Graph::new(); + gg.add_node(1, "Y", NO_PROPS, None).unwrap(); + + let res = gg.import_node_as(&g_a, "X", false).unwrap(); + assert_eq!(res.name(), "X"); + assert_eq!(res.history(), vec![0]); + + let res = gg.import_node_as(&g_b, "Y", true).unwrap(); + assert_eq!(res.name(), "Y"); + assert_eq!(res.history(), vec![1]); + assert_eq!(res.properties().get("temp").unwrap(), Prop::Bool(true)); + assert_eq!( + res.properties().constant().get("con").unwrap(), + Prop::I64(11) + ); + } + + #[test] + fn import_nodes_as() { + let g = Graph::new(); + let g_a = g.add_node(0, "A", NO_PROPS, None).unwrap(); + let g_b = g + .add_node(1, "B", vec![("temp".to_string(), Prop::Bool(true))], None) + .unwrap(); + let _ = g_b.add_constant_properties(vec![("con".to_string(), Prop::I64(11))]); + let g_c = g.add_node(0, "C", NO_PROPS, None).unwrap(); + + let gg = Graph::new(); + gg.add_node(1, "Q", NO_PROPS, None).unwrap(); + gg.add_node(1, "R", NO_PROPS, None).unwrap(); + let res = gg.import_nodes_as(vec![&g_a, &g_b, &g_c], vec!["P", "Q", "R"], false); + match res { + Err(NodesExistError(ids)) => { + assert_eq!( + ids.into_iter() + .map(|id| id.to_string()) + .collect::>(), + vec!["Q", "R"], + ); + } + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(_) => panic!("Expected error but got Ok"), + } + let mut nodes = gg.nodes().name().collect_vec(); + nodes.sort(); + assert_eq!(nodes, vec!["Q", "R"]); // Nodes up until first failure are imported + let y = gg.node("Q").unwrap(); + assert_eq!(y.name(), "Q"); + assert_eq!(y.history(), vec![1]); + assert_eq!(y.properties().get("temp"), None); + assert_eq!(y.properties().constant().get("con"), None); + } + + #[test] + fn import_nodes_as_merge() { + let g = Graph::new(); + let g_a = g.add_node(0, "A", NO_PROPS, None).unwrap(); + let g_b = g + .add_node(1, "B", vec![("temp".to_string(), Prop::Bool(true))], None) + .unwrap(); + let _ = g_b.add_constant_properties(vec![("con".to_string(), Prop::I64(11))]); + + let gg = Graph::new(); + gg.add_node(1, "Q", NO_PROPS, None).unwrap(); + let _ = gg + .import_nodes_as(vec![&g_a, &g_b], vec!["P", "Q"], true) + .unwrap(); + let mut nodes = gg.nodes().name().collect_vec(); + nodes.sort(); + assert_eq!(nodes, vec!["P", "Q"]); + let y = gg.node("Q").unwrap(); + assert_eq!(y.name(), "Q"); + assert_eq!(y.history(), vec![1]); + assert_eq!(y.properties().get("temp").unwrap(), Prop::Bool(true)); + assert_eq!(y.properties().constant().get("con").unwrap(), Prop::I64(11)); + } + + #[test] + fn import_edge_as() { + let g = Graph::new(); + let g_a = g.add_node(0, "A", NO_PROPS, None).unwrap(); + let g_b = g + .add_node(1, "B", vec![("temp".to_string(), Prop::Bool(true))], None) + .unwrap(); + let _ = g_b.add_constant_properties(vec![("con".to_string(), Prop::I64(11))]); + let e_a_b = g + .add_edge( + 2, + "A", + "B", + vec![("e_temp".to_string(), Prop::Bool(false))], + None, + ) + .unwrap(); + let e_b_c = g + .add_edge( + 2, + "B", + "C", + vec![("e_temp".to_string(), Prop::Bool(false))], + None, + ) + .unwrap(); + + let gg = Graph::new(); + let e = gg.add_edge(1, "X", "Y", NO_PROPS, None).unwrap(); + let res = gg.import_edge_as(&e_b_c, ("Y", "Z"), false); + let res = gg.import_edge_as(&e_a_b, ("X", "Y"), false); + match res { + Err(EdgeExistsError(src_id, dst_id)) => { + assert_eq!(src_id.to_string(), "X"); + assert_eq!(dst_id.to_string(), "Y"); + } + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(_) => panic!("Expected error but got Ok"), + } + let mut nodes = gg.nodes().name().collect_vec(); + nodes.sort(); + assert_eq!(nodes, vec!["X", "Y", "Z"]); + let x = gg.node("X").unwrap(); + assert_eq!(x.name(), "X"); + assert_eq!(x.history(), vec![1]); + let y = gg.node("Y").unwrap(); + assert_eq!(y.name(), "Y"); + assert_eq!(y.history(), vec![1, 2]); + assert_eq!(y.properties().get("temp"), None); + assert_eq!(y.properties().constant().get("con"), None); + + let e_src = gg.edge("X", "Y").unwrap().src().name(); + let e_dst = gg.edge("X", "Y").unwrap().dst().name(); + assert_eq!(e_src, "X"); + assert_eq!(e_dst, "Y"); + + let props = gg.edge("X", "Y").unwrap().properties().as_vec(); + assert_eq!(props, vec![]); + } + + #[test] + fn import_edge_as_merge() { + let g = Graph::new(); + let g_a = g.add_node(0, "A", NO_PROPS, None).unwrap(); + let g_b = g + .add_node(1, "B", vec![("temp".to_string(), Prop::Bool(true))], None) + .unwrap(); + let _ = g_b.add_constant_properties(vec![("con".to_string(), Prop::I64(11))]); + let e_a_b = g + .add_edge( + 2, + "A", + "B", + vec![("e_temp".to_string(), Prop::Bool(false))], + None, + ) + .unwrap(); + + let gg = Graph::new(); + let _ = gg.add_edge(3, "X", "Y", NO_PROPS, None).unwrap(); + let res = gg.import_edge_as(&e_a_b, ("X", "Y"), true).unwrap(); + assert_eq!(res.src().name(), "X"); + assert_eq!(res.dst().name(), "Y"); + assert_eq!(res.properties().as_vec(), e_a_b.properties().as_vec()); + let mut nodes = gg.nodes().name().collect_vec(); + nodes.sort(); + assert_eq!(nodes, vec!["X", "Y"]); + let x = gg.node("X").unwrap(); + assert_eq!(x.name(), "X"); + assert_eq!(x.history(), vec![2, 3]); + let y = gg.node("Y").unwrap(); + assert_eq!(y.name(), "Y"); + assert_eq!(y.history(), vec![2, 3]); + assert_eq!(y.properties().get("temp"), None); + assert_eq!(y.properties().constant().get("con"), None); + } + + #[test] + fn import_edges_as() { + let g = Graph::new(); + let g_a = g.add_node(0, "A", NO_PROPS, None).unwrap(); + let g_b = g + .add_node(1, "B", vec![("temp".to_string(), Prop::Bool(true))], None) + .unwrap(); + let _ = g_b.add_constant_properties(vec![("con".to_string(), Prop::I64(11))]); + let g_c = g.add_node(0, "C", NO_PROPS, None).unwrap(); + let e_a_b = g + .add_edge( + 2, + "A", + "B", + vec![("e_temp".to_string(), Prop::Bool(false))], + None, + ) + .unwrap(); + let e_b_c = g.add_edge(2, "B", "C", NO_PROPS, None).unwrap(); + + let gg = Graph::new(); + let e = gg.add_edge(1, "Y", "Z", NO_PROPS, None).unwrap(); + let res = gg.import_edges_as([&e_a_b, &e_b_c], [("X", "Y"), ("Y", "Z")], false); + match res { + Err(GraphError::EdgesExistError(duplicates)) => { + assert_eq!( + duplicates + .into_iter() + .map(|(x, y)| (x.to_string(), y.to_string())) + .collect::>(), + vec![("Y".to_string(), "Z".to_string())] + ); + } + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(_) => panic!("Expected error but got Ok"), + } + let mut nodes = gg.nodes().name().collect_vec(); + nodes.sort(); + assert_eq!(nodes, vec!["Y", "Z"]); + let y = gg.node("Y").unwrap(); + assert_eq!(y.name(), "Y"); + assert_eq!(y.history(), vec![1]); + assert_eq!(y.properties().get("temp"), None); + assert_eq!(y.properties().constant().get("con"), None); + let x = gg.node("Z").unwrap(); + assert_eq!(x.name(), "Z"); + assert_eq!(x.history(), vec![1]); + + assert!(gg.edge("X", "Y").is_none()); + + let e_y_z = gg.edge("Y", "Z").unwrap(); + assert_eq!( + (e_y_z.src().name().as_str(), e_y_z.dst().name().as_str()), + ("Y", "Z") + ); + + let props = e_y_z.properties().as_vec(); + assert_eq!(props, vec![]); + } + + #[test] + fn import_edges_as_merge() { + let g = Graph::new(); + let g_a = g.add_node(0, "A", NO_PROPS, None).unwrap(); + let g_b = g + .add_node(1, "B", vec![("temp".to_string(), Prop::Bool(true))], None) + .unwrap(); + let _ = g_b.add_constant_properties(vec![("con".to_string(), Prop::I64(11))]); + let e_a_b = g + .add_edge( + 2, + "A", + "B", + vec![("e_temp".to_string(), Prop::Bool(false))], + None, + ) + .unwrap(); + + let gg = Graph::new(); + let _ = gg.add_edge(3, "X", "Y", NO_PROPS, None).unwrap(); + let res = gg.import_edges_as([&e_a_b], [("X", "Y")], true).unwrap(); + + let e_x_y = gg.edge("X", "Y").unwrap(); + assert_eq!( + (e_x_y.src().name().as_str(), e_x_y.dst().name().as_str()), + ("X", "Y") + ); + assert_eq!(e_x_y.properties().get("e_temp").unwrap(), Prop::Bool(false)); + + let mut nodes = gg.nodes().name().collect_vec(); + nodes.sort(); + assert_eq!(nodes, vec!["X", "Y"]); + let x = gg.node("X").unwrap(); + assert_eq!(x.name(), "X"); + assert_eq!(x.history(), vec![2, 3]); + let y = gg.node("Y").unwrap(); + assert_eq!(y.name(), "Y"); + assert_eq!(y.history(), vec![2, 3]); + assert_eq!(y.properties().get("temp"), None); + assert_eq!(y.properties().constant().get("con"), None); } #[test] diff --git a/raphtory/src/python/graph/graph.rs b/raphtory/src/python/graph/graph.rs index 44b32cf6d..d7b7495c7 100644 --- a/raphtory/src/python/graph/graph.rs +++ b/raphtory/src/python/graph/graph.rs @@ -177,8 +177,12 @@ impl PyGraph { /// id (str|int): The id of the node. /// properties (PropInput, optional): The properties of the node. /// node_type (str, optional): The optional string which will be used as a node type + /// /// Returns: - /// MutableNode: The added node + /// MutableNode: The added node. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3(signature = (timestamp, id, properties = None, node_type = None))] pub fn add_node( &self, @@ -198,8 +202,12 @@ impl PyGraph { /// id (str|int): The id of the node. /// properties (PropInput, optional): The properties of the node. /// node_type (str, optional): The optional string which will be used as a node type + /// /// Returns: - /// MutableNode: The created node + /// MutableNode: The created node. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3(signature = (timestamp, id, properties = None, node_type = None))] pub fn create_node( &self, @@ -217,6 +225,12 @@ impl PyGraph { /// Arguments: /// timestamp (TimeInput): The timestamp of the temporal property. /// properties (PropInput): The temporal properties of the graph. + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. pub fn add_property( &self, timestamp: PyTime, @@ -229,6 +243,12 @@ impl PyGraph { /// /// Arguments: /// properties (PropInput): The static properties of the graph. + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. pub fn add_constant_properties( &self, properties: HashMap, @@ -241,6 +261,11 @@ impl PyGraph { /// Arguments: /// properties (PropInput): The static properties of the graph. /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. pub fn update_constant_properties( &self, properties: HashMap, @@ -258,7 +283,10 @@ impl PyGraph { /// layer (str, optional): The layer of the edge. /// /// Returns: - /// MutableEdge: The added edge + /// MutableEdge: The added edge. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3(signature = (timestamp, src, dst, properties = None, layer = None))] pub fn add_edge( &self, @@ -274,74 +302,182 @@ impl PyGraph { /// Import a single node into the graph. /// - /// This function takes a PyNode object and an optional boolean flag. If the flag is set to true, - /// the function will force the import of the node even if it already exists in the graph. - /// /// Arguments: /// node (Node): A Node object representing the node to be imported. - /// force (bool): An optional boolean flag indicating whether to force the import of the node. + /// merge (bool): An optional boolean flag. + /// If merge is false, the function will return an error if the imported node already exists in the graph. + /// If merge is true, the function merges the histories of the imported node and the existing node (in the graph). /// /// Returns: - /// Node: A Result object which is Ok if the node was successfully imported, and Err otherwise. - #[pyo3(signature = (node, force = false))] + /// Node: A node object if the node was successfully imported. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (node, merge = false))] pub fn import_node( &self, node: PyNode, - force: bool, + merge: bool, + ) -> Result, GraphError> { + self.graph.import_node(&node.node, merge) + } + + /// Import a single node into the graph with new id. + /// + /// Arguments: + /// node (Node): A Node object representing the node to be imported. + /// new_id (str|int): The new node id. + /// merge (bool): An optional boolean flag. + /// If merge is false, the function will return an error if the imported node already exists in the graph. + /// If merge is true, the function merges the histories of the imported node and the existing node (in the graph). + /// + /// Returns: + /// Node: A node object if the node was successfully imported. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (node, new_id, merge = false))] + pub fn import_node_as( + &self, + node: PyNode, + new_id: GID, + merge: bool, ) -> Result, GraphError> { - self.graph.import_node(&node.node, force) + self.graph.import_node_as(&node.node, new_id, merge) } /// Import multiple nodes into the graph. /// - /// This function takes a vector of PyNode objects and an optional boolean flag. If the flag is set to true, - /// the function will force the import of the nodes even if they already exist in the graph. + /// Arguments: + /// nodes (List[Node]): A vector of Node objects representing the nodes to be imported. + /// merge (bool): An optional boolean flag. + /// If merge is false, the function will return an error if any of the imported nodes already exists in the graph. + /// If merge is true, the function merges the histories of the imported nodes and the existing nodes (in the graph). + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (nodes, merge = false))] + pub fn import_nodes(&self, nodes: FromIterable, merge: bool) -> Result<(), GraphError> { + let node_views = nodes.iter().map(|node| &node.node); + self.graph.import_nodes(node_views, merge) + } + + /// Import multiple nodes into the graph with new ids. /// /// Arguments: + /// nodes (List[Node]): A vector of Node objects representing the nodes to be imported. + /// new_ids (List[str|int]): A list of node IDs to use for the imported nodes. + /// merge (bool): An optional boolean flag. + /// If merge is false, the function will return an error if any of the imported nodes already exists in the graph. + /// If merge is true, the function merges the histories of the imported nodes and the existing nodes (in the graph). /// - /// nodes (List[Node]): A vector of PyNode objects representing the nodes to be imported. - /// force (bool): An optional boolean flag indicating whether to force the import of the nodes. + /// Returns: + /// None: This function does not return a value, if the operation is successful. /// - #[pyo3(signature = (nodes, force = false))] - pub fn import_nodes(&self, nodes: FromIterable, force: bool) -> Result<(), GraphError> { + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (nodes, new_ids, merge = false))] + pub fn import_nodes_as( + &self, + nodes: Vec, + new_ids: Vec, + merge: bool, + ) -> Result<(), GraphError> { let node_views = nodes.iter().map(|node| &node.node); - self.graph.import_nodes(node_views, force) + self.graph.import_nodes_as(node_views, new_ids, merge) } /// Import a single edge into the graph. /// - /// This function takes a PyEdge object and an optional boolean flag. If the flag is set to true, - /// the function will force the import of the edge even if it already exists in the graph. - /// /// Arguments: - /// - /// edge (Edge): A PyEdge object representing the edge to be imported. - /// force (bool): An optional boolean flag indicating whether to force the import of the edge. + /// edge (Edge): A Edge object representing the edge to be imported. + /// merge (bool): An optional boolean flag. + /// If merge is false, the function will return an error if the imported edge already exists in the graph. + /// If merge is true, the function merges the histories of the imported edge and the existing edge (in the graph). /// /// Returns: - /// Edge: A Result object which is Ok if the edge was successfully imported, and Err otherwise. - #[pyo3(signature = (edge, force = false))] + /// EdgeView: An EdgeView object if the edge was successfully imported. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (edge, merge = false))] pub fn import_edge( &self, edge: PyEdge, - force: bool, + merge: bool, ) -> Result, GraphError> { - self.graph.import_edge(&edge.edge, force) + self.graph.import_edge(&edge.edge, merge) } - /// Import multiple edges into the graph. + /// Import a single edge into the graph with new id. /// - /// This function takes a vector of PyEdge objects and an optional boolean flag. If the flag is set to true, - /// the function will force the import of the edges even if they already exist in the graph. + /// Arguments: + /// edge (Edge): A Edge object representing the edge to be imported. + /// new_id (tuple) : The ID of the new edge. It's a tuple of the source and destination node ids. + /// merge (bool): An optional boolean flag. + /// If merge is false, the function will return an error if the imported edge already exists in the graph. + /// If merge is true, the function merges the histories of the imported edge and the existing edge (in the graph). + /// + /// Returns: + /// EdgeView: An EdgeView object if the edge was successfully imported. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (edge, new_id, merge = false))] + pub fn import_edge_as( + &self, + edge: PyEdge, + new_id: (GID, GID), + merge: bool, + ) -> Result, GraphError> { + self.graph.import_edge_as(&edge.edge, new_id, merge) + } + + /// Import multiple edges into the graph. /// /// Arguments: + /// edges (List[Edge]): A list of Edge objects representing the edges to be imported. + /// merge (bool): An optional boolean flag. + /// If merge is false, the function will return an error if any of the imported edges already exists in the graph. + /// If merge is true, the function merges the histories of the imported edges and the existing edges (in the graph). + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (edges, merge = false))] + pub fn import_edges(&self, edges: FromIterable, merge: bool) -> Result<(), GraphError> { + let edge_views = edges.iter().map(|edge| &edge.edge); + self.graph.import_edges(edge_views, merge) + } + + /// Import multiple edges into the graph with new ids. /// + /// Arguments: /// edges (List[Edge]): A list of Edge objects representing the edges to be imported. - /// force (bool): An optional boolean flag indicating whether to force the import of the edges. - #[pyo3(signature = (edges, force = false))] - pub fn import_edges(&self, edges: FromIterable, force: bool) -> Result<(), GraphError> { + /// new_ids (List[tuple]) - The IDs of the new edges. It's a vector of tuples of the source and destination node ids. + /// merge (bool): An optional boolean flag. + /// If merge is false, the function will return an error if any of the imported edges already exists in the graph. + /// If merge is true, the function merges the histories of the imported edges and the existing edges (in the graph). + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (edges, new_ids, merge = false))] + pub fn import_edges_as( + &self, + edges: Vec, + new_ids: Vec<(GID, GID)>, + merge: bool, + ) -> Result<(), GraphError> { let edge_views = edges.iter().map(|edge| &edge.edge); - self.graph.import_edges(edge_views, force) + self.graph.import_edges_as(edge_views, new_ids, merge) } //FIXME: This is reimplemented here to get mutable views. If we switch the underlying graph to enum dispatch, this won't be necessary! @@ -351,7 +487,7 @@ impl PyGraph { /// id (str|int): the node id /// /// Returns: - /// Node: the node with the specified id, or None if the node does not exist + /// Node: The node object with the specified id, or None if the node does not exist pub fn node(&self, id: PyNodeRef) -> Option> { self.graph.node(id) } @@ -414,6 +550,12 @@ impl PyGraph { /// properties (List[str]): List of node property column names. Defaults to None. (optional) /// constant_properties (List[str]): List of constant node property column names. Defaults to None. (optional) /// shared_constant_properties (PropInput): A dictionary of constant properties that will be added to every node. Defaults to None. (optional) + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3( signature = (df,time, id, node_type = None, node_type_col = None, properties = None, constant_properties = None, shared_constant_properties = None) )] @@ -454,6 +596,12 @@ impl PyGraph { /// properties (List[str]): List of node property column names. Defaults to None. (optional) /// constant_properties (List[str]): List of constant node property column names. Defaults to None. (optional) /// shared_constant_properties (PropInput): A dictionary of constant properties that will be added to every node. Defaults to None. (optional) + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3( signature = (parquet_path, time, id, node_type = None, node_type_col = None, properties = None, constant_properties = None, shared_constant_properties = None) )] @@ -495,6 +643,12 @@ impl PyGraph { /// shared_constant_properties (PropInput): A dictionary of constant properties that will be added to every edge. Defaults to None. (optional) /// layer (str): A constant value to use as the layer for all edges (optional) Defaults to None. (cannot be used in combination with layer_col) /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. (cannot be used in combination with layer) + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3( signature = (df, time, src, dst, properties = None, constant_properties = None, shared_constant_properties = None, layer = None, layer_col = None) )] @@ -538,6 +692,12 @@ impl PyGraph { /// shared_constant_properties (PropInput): A dictionary of constant properties that will be added to every edge. Defaults to None. (optional) /// layer (str): A constant value to use as the layer for all edges (optional) Defaults to None. (cannot be used in combination with layer_col) /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. (cannot be used in combination with layer) + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3( signature = (parquet_path, time, src, dst, properties = None, constant_properties = None, shared_constant_properties = None, layer = None, layer_col = None) )] @@ -578,6 +738,12 @@ impl PyGraph { /// node_type_col (str): The node type col name in dataframe (optional) Defaults to None. (cannot be used in combination with node_type) /// constant_properties (List[str]): List of constant node property column names. Defaults to None. (optional) /// shared_constant_properties (PropInput): A dictionary of constant properties that will be added to every node. Defaults to None. (optional) + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3(signature = (df, id, node_type=None, node_type_col=None, constant_properties = None, shared_constant_properties = None))] fn load_node_props_from_pandas( &self, @@ -609,6 +775,12 @@ impl PyGraph { /// node_type_col (str): The node type col name in dataframe (optional) Defaults to None. (cannot be used in combination with node_type) /// constant_properties (List[str]): List of constant node property column names. Defaults to None. (optional) /// shared_constant_properties (PropInput): A dictionary of constant properties that will be added to every node. Defaults to None. (optional) + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3(signature = (parquet_path, id, node_type=None,node_type_col=None, constant_properties = None, shared_constant_properties = None))] fn load_node_props_from_parquet( &self, @@ -641,6 +813,12 @@ impl PyGraph { /// shared_constant_properties (PropInput): A dictionary of constant properties that will be added to every edge. Defaults to None. (optional) /// layer (str): The edge layer name (optional) Defaults to None. /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3( signature = (df, src, dst, constant_properties = None, shared_constant_properties = None, layer = None, layer_col = None) )] @@ -677,6 +855,12 @@ impl PyGraph { /// shared_constant_properties (PropInput): A dictionary of constant properties that will be added to every edge. Defaults to None. (optional) /// layer (str): The edge layer name (optional) Defaults to None. /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3( signature = (parquet_path, src, dst, constant_properties = None, shared_constant_properties = None, layer = None, layer_col = None) )] diff --git a/raphtory/src/python/graph/graph_with_deletions.rs b/raphtory/src/python/graph/graph_with_deletions.rs index eb12e305a..1ea4aeb18 100644 --- a/raphtory/src/python/graph/graph_with_deletions.rs +++ b/raphtory/src/python/graph/graph_with_deletions.rs @@ -118,7 +118,10 @@ impl PyPersistentGraph { /// node_type (str) : The optional string which will be used as a node type /// /// Returns: - /// None + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3(signature = (timestamp, id, properties = None, node_type = None))] pub fn add_node( &self, @@ -141,6 +144,9 @@ impl PyPersistentGraph { /// /// Returns: /// MutableNode + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3(signature = (timestamp, id, properties = None, node_type = None))] pub fn create_node( &self, @@ -160,7 +166,10 @@ impl PyPersistentGraph { /// properties (dict): The temporal properties of the graph. /// /// Returns: - /// None + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. pub fn add_property( &self, timestamp: PyTime, @@ -175,7 +184,10 @@ impl PyPersistentGraph { /// properties (dict): The static properties of the graph. /// /// Returns: - /// None + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. pub fn add_constant_properties( &self, properties: HashMap, @@ -189,7 +201,10 @@ impl PyPersistentGraph { /// properties (dict): The static properties of the graph. /// /// Returns: - /// None + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. pub fn update_constant_properties( &self, properties: HashMap, @@ -207,7 +222,10 @@ impl PyPersistentGraph { /// layer (str): The layer of the edge. /// /// Returns: - /// None + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3(signature = (timestamp, src, dst, properties = None, layer = None))] pub fn add_edge( &self, @@ -231,6 +249,9 @@ impl PyPersistentGraph { /// /// Returns: /// The deleted edge + /// + /// Raises: + /// GraphError: If the operation fails. #[pyo3(signature = (timestamp, src, dst, layer=None))] pub fn delete_edge( &self, @@ -249,7 +270,7 @@ impl PyPersistentGraph { /// id (str | int): the node id /// /// Returns: - /// the node with the specified id, or None if the node does not exist + /// The node with the specified id, or None if the node does not exist pub fn node(&self, id: PyNodeRef) -> Option> { self.graph.node(id) } @@ -262,7 +283,7 @@ impl PyPersistentGraph { /// dst (str | int): the destination node id /// /// Returns: - /// the edge with the specified source and destination nodes, or None if the edge does not exist + /// The edge with the specified source and destination nodes, or None if the edge does not exist #[pyo3(signature = (src, dst))] pub fn edge( &self, @@ -274,75 +295,189 @@ impl PyPersistentGraph { /// Import a single node into the graph. /// - /// This function takes a PyNode object and an optional boolean flag. If the flag is set to true, - /// the function will force the import of the node even if it already exists in the graph. + /// This function takes a node object and an optional boolean flag. If the flag is set to true, + /// the function will merge the import of the node even if it already exists in the graph. /// /// Arguments: - /// node (Node): A PyNode object representing the node to be imported. - /// force (bool): An optional boolean flag indicating whether to force the import of the node. + /// node (Node): A node object representing the node to be imported. + /// merge (bool): An optional boolean flag indicating whether to merge the import of the node. Defaults to False. /// /// Returns: - /// Result, GraphError> - A Result object which is Ok if the node was successfully imported, and Err otherwise. - #[pyo3(signature = (node, force = false))] + /// NodeView: A nodeview object if the node was successfully imported, and an error otherwise. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (node, merge = false))] pub fn import_node( &self, node: PyNode, - force: bool, + merge: bool, ) -> Result, GraphError> { - self.graph.import_node(&node.node, force) + self.graph.import_node(&node.node, merge) + } + + /// Import a single node into the graph with new id. + /// + /// This function takes a node object, a new node id and an optional boolean flag. If the flag is set to true, + /// the function will merge the import of the node even if it already exists in the graph. + /// + /// Arguments: + /// node (Node): A node object representing the node to be imported. + /// new_id (str|int): The new node id. + /// merge (bool): An optional boolean flag indicating whether to merge the import of the node. Defaults to False. + /// + /// Returns: + /// NodeView: A nodeview object if the node was successfully imported, and an error otherwise. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (node, new_id, merge = false))] + pub fn import_node_as( + &self, + node: PyNode, + new_id: GID, + merge: bool, + ) -> Result, GraphError> { + self.graph.import_node_as(&node.node, new_id, merge) } /// Import multiple nodes into the graph. /// - /// This function takes a vector of PyNode objects and an optional boolean flag. If the flag is set to true, - /// the function will force the import of the nodes even if they already exist in the graph. + /// This function takes a vector of node objects and an optional boolean flag. If the flag is set to true, + /// the function will merge the import of the nodes even if they already exist in the graph. /// /// Arguments: + /// nodes (List[Node]): A vector of node objects representing the nodes to be imported. + /// merge (bool): An optional boolean flag indicating whether to merge the import of the nodes. Defaults to False. /// - /// nodes (List[Node]): A vector of PyNode objects representing the nodes to be imported. - /// force (bool): An optional boolean flag indicating whether to force the import of the nodes. + /// Returns: + /// None: This function does not return a value, if the operation is successful. /// - #[pyo3(signature = (nodes, force = false))] - pub fn import_nodes(&self, nodes: Vec, force: bool) -> Result<(), GraphError> { + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (nodes, merge = false))] + pub fn import_nodes(&self, nodes: Vec, merge: bool) -> Result<(), GraphError> { let node_views = nodes.iter().map(|node| &node.node); - self.graph.import_nodes(node_views, force) + self.graph.import_nodes(node_views, merge) } - /// Import a single edge into the graph. + /// Import multiple nodes into the graph with new ids. /// - /// This function takes a PyEdge object and an optional boolean flag. If the flag is set to true, - /// the function will force the import of the edge even if it already exists in the graph. + /// This function takes a vector of node objects, a list of new node ids and an optional boolean flag. If the flag is set to true, + /// the function will merge the import of the nodes even if they already exist in the graph. /// /// Arguments: + /// nodes (List[Node]): A vector of node objects representing the nodes to be imported. + /// new_ids (List[str|int]): A list of node IDs to use for the imported nodes. + /// merge (bool): An optional boolean flag indicating whether to merge the import of the nodes. Defaults to False. /// - /// edge (Edge): A PyEdge object representing the edge to be imported. - /// force (bool): An optional boolean flag indicating whether to force the import of the edge. + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (nodes, new_ids, merge = false))] + pub fn import_nodes_as( + &self, + nodes: Vec, + new_ids: Vec, + merge: bool, + ) -> Result<(), GraphError> { + let node_views = nodes.iter().map(|node| &node.node); + self.graph.import_nodes_as(node_views, new_ids, merge) + } + + /// Import a single edge into the graph. + /// + /// This function takes an edge object and an optional boolean flag. If the flag is set to true, + /// the function will merge the import of the edge even if it already exists in the graph. + /// + /// Arguments: + /// edge (Edge): An edge object representing the edge to be imported. + /// merge (bool): An optional boolean flag indicating whether to merge the import of the edge. Defaults to False. /// /// Returns: /// Edge: The imported edge. - #[pyo3(signature = (edge, force = false))] + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (edge, merge = false))] pub fn import_edge( &self, edge: PyEdge, - force: bool, + merge: bool, + ) -> Result, GraphError> { + self.graph.import_edge(&edge.edge, merge) + } + + /// Import a single edge into the graph with new id. + /// + /// This function takes a edge object, a new edge id and an optional boolean flag. If the flag is set to true, + /// the function will merge the import of the edge even if it already exists in the graph. + /// + /// Arguments: + /// edge (Edge): A edge object representing the edge to be imported. + /// new_id (tuple) : The ID of the new edge. It's a tuple of the source and destination node ids. + /// merge (bool): An optional boolean flag indicating whether to merge the import of the edge. Defaults to False. + /// + /// Returns: + /// Edge: The imported edge. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (edge, new_id, merge = false))] + pub fn import_edge_as( + &self, + edge: PyEdge, + new_id: (GID, GID), + merge: bool, ) -> Result, GraphError> { - self.graph.import_edge(&edge.edge, force) + self.graph.import_edge_as(&edge.edge, new_id, merge) } /// Import multiple edges into the graph. /// - /// This function takes a vector of PyEdge objects and an optional boolean flag. If the flag is set to true, - /// the function will force the import of the edges even if they already exist in the graph. + /// This function takes a vector of edge objects and an optional boolean flag. If the flag is set to true, + /// the function will merge the import of the edges even if they already exist in the graph. /// /// Arguments: + /// edges (List[Edge]): A vector of edge objects representing the edges to be imported. + /// merge (bool): An optional boolean flag indicating whether to merge the import of the edges. Defaults to False. /// - /// edges (List[Edge]): A vector of PyEdge objects representing the edges to be imported. - /// force (bool): An optional boolean flag indicating whether to force the import of the edges. + /// Returns: + /// None: This function does not return a value, if the operation is successful. /// - #[pyo3(signature = (edges, force = false))] - pub fn import_edges(&self, edges: Vec, force: bool) -> Result<(), GraphError> { + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (edges, merge = false))] + pub fn import_edges(&self, edges: Vec, merge: bool) -> Result<(), GraphError> { let edge_views = edges.iter().map(|edge| &edge.edge); - self.graph.import_edges(edge_views, force) + self.graph.import_edges(edge_views, merge) + } + + /// Import multiple edges into the graph with new ids. + /// + /// This function takes a vector of edge objects, a list of new edge ids and an optional boolean flag. If the flag is set to true, + /// the function will merge the import of the edges even if they already exist in the graph. + /// + /// Arguments: + /// edges (List[Edge]): A vector of edge objects representing the edges to be imported. + /// merge (bool): An optional boolean flag indicating whether to merge the import of the edges. Defaults to False. + /// + /// Returns: + /// None: This function does not return a value, if the operation is successful. + /// + /// Raises: + /// GraphError: If the operation fails. + #[pyo3(signature = (edges, new_ids, merge = false))] + pub fn import_edges_as( + &self, + edges: Vec, + new_ids: Vec<(GID, GID)>, + merge: bool, + ) -> Result<(), GraphError> { + let edge_views = edges.iter().map(|edge| &edge.edge); + self.graph.import_edges_as(edge_views, new_ids, merge) } //****** Saving And Loading ******// @@ -377,8 +512,9 @@ impl PyPersistentGraph { /// properties (List[str]): List of node property column names. Defaults to None. (optional) /// constant_properties (List[str]): List of constant node property column names. Defaults to None. (optional) /// shared_constant_properties (dict): A dictionary of constant properties that will be added to every node. Defaults to None. (optional) + /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails. @@ -420,8 +556,9 @@ impl PyPersistentGraph { /// properties (List[str]): List of node property column names. Defaults to None. (optional) /// constant_properties (List[str]): List of constant node property column names. Defaults to None. (optional) /// shared_constant_properties (dict): A dictionary of constant properties that will be added to every node. Defaults to None. (optional) + /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails. @@ -464,8 +601,9 @@ impl PyPersistentGraph { /// shared_constant_properties (dict): A dictionary of constant properties that will be added to every edge. Defaults to None. (optional) /// layer (str): A constant value to use as the layer for all edges (optional) Defaults to None. (cannot be used in combination with layer_col) /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. (cannot be used in combination with layer) + /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails. @@ -510,8 +648,9 @@ impl PyPersistentGraph { /// shared_constant_properties (dict): A dictionary of constant properties that will be added to every edge. Defaults to None. (optional) /// layer (str): A constant value to use as the layer for all edges (optional) Defaults to None. (cannot be used in combination with layer_col) /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. (cannot be used in combination with layer) + /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails. @@ -553,8 +692,9 @@ impl PyPersistentGraph { /// dst (str): The column name for the destination node ids. /// layer (str): A constant value to use as the layer for all edges (optional) Defaults to None. (cannot be used in combination with layer_col) /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. (cannot be used in combination with layer) + /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails. @@ -580,8 +720,9 @@ impl PyPersistentGraph { /// time (str): The column name for the update timestamps. /// layer (str): A constant value to use as the layer for all edges (optional) Defaults to None. (cannot be used in combination with layer_col) /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. (cannot be used in combination with layer) + /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails. @@ -617,7 +758,7 @@ impl PyPersistentGraph { /// shared_constant_properties (dict): A dictionary of constant properties that will be added to every node. Defaults to None. (optional) /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails. @@ -654,7 +795,7 @@ impl PyPersistentGraph { /// shared_constant_properties (dict): A dictionary of constant properties that will be added to every node. Defaults to None. (optional) /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails. @@ -692,7 +833,7 @@ impl PyPersistentGraph { /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails. @@ -732,7 +873,7 @@ impl PyPersistentGraph { /// layer_col (str): The edge layer col name in dataframe (optional) Defaults to None. /// /// Returns: - /// None: If the operation is successful. + /// None: This function does not return a value, if the operation is successful. /// /// Raises: /// GraphError: If the operation fails.