From a9a16ba2927c0063d618165c93ba10bf084f7655 Mon Sep 17 00:00:00 2001 From: Shivam <4599890+shivam-880@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:51:00 +0000 Subject: [PATCH] Feature/create node (#1855) * add create_node and test * add create node to py * add create node to graphql * fmt * ref * ref * add tests * fmt --------- Co-authored-by: Shivam Kapoor <4599890+iamsmkr@users.noreply.github.com> --- .../tests/graphql/edit_graph/test_graphql.py | 174 +++++++++++++++- python/tests/test_graphdb/test_graphdb.py | 187 ++++++++++-------- .../src/model/graph/mutable_graph.rs | 19 ++ .../src/python/client/raphtory_client.rs | 7 +- .../src/python/client/remote_graph.rs | 51 ++++- raphtory/src/db/api/mutation/addition_ops.rs | 40 ++++ raphtory/src/db/graph/graph.rs | 15 ++ raphtory/src/python/graph/graph.rs | 21 ++ .../src/python/graph/graph_with_deletions.rs | 22 +++ 9 files changed, 443 insertions(+), 93 deletions(-) diff --git a/python/tests/graphql/edit_graph/test_graphql.py b/python/tests/graphql/edit_graph/test_graphql.py index e1777f3cc..28b36f959 100644 --- a/python/tests/graphql/edit_graph/test_graphql.py +++ b/python/tests/graphql/edit_graph/test_graphql.py @@ -3,7 +3,7 @@ import pytest -from raphtory.graphql import GraphServer, RaphtoryClient, encode_graph +from raphtory.graphql import GraphServer, RaphtoryClient, encode_graph, RemoteGraph from raphtory import graph_loader from raphtory import Graph import json @@ -21,8 +21,8 @@ def test_encode_graph(): encoded = encode_graph(g) assert ( - encoded - == "EgxaCgoIX2RlZmF1bHQSDBIKCghfZGVmYXVsdBoFCgNiZW4aCQoFaGFtemEYARoLCgdoYWFyb29uGAIiAhABIgYIAhABGAEiBBACGAIqAhoAKgQSAhABKgQSAhADKgIKACoGEgQIARABKgYSBAgBEAIqBAoCCAEqBhIECAIQAioGEgQIAhADKgQKAggCKgQ6AhABKgIyACoIOgYIARACGAEqBDICCAEqCDoGCAIQAxgCKgQyAggC" + encoded + == "EgxaCgoIX2RlZmF1bHQSDBIKCghfZGVmYXVsdBoFCgNiZW4aCQoFaGFtemEYARoLCgdoYWFyb29uGAIiAhABIgYIAhABGAEiBBACGAIqAhoAKgQSAhABKgQSAhADKgIKACoGEgQIARABKgYSBAgBEAIqBAoCCAEqBhIECAIQAioGEgQIAhADKgQKAggCKgQ6AhABKgIyACoIOgYIARACGAEqBDICCAEqCDoGCAIQAxgCKgQyAggC" ) @@ -42,8 +42,8 @@ def test_wrong_url(): with pytest.raises(Exception) as excinfo: client = RaphtoryClient("http://broken_url.com") assert ( - str(excinfo.value) - == "Could not connect to the given server - no response --error sending request for url (http://broken_url.com/)" + str(excinfo.value) + == "Could not connect to the given server - no response --error sending request for url (http://broken_url.com/)" ) @@ -382,6 +382,170 @@ def test_graph_properties_query(): ) +def test_create_node(): + g = Graph() + g.add_edge(1, "ben", "shivam") + + tmp_work_dir = tempfile.mkdtemp() + with GraphServer(tmp_work_dir).start(port=1737): + client = RaphtoryClient("http://localhost:1737") + client.send_graph(path="g", graph=g) + + query_nodes = """{graph(path: "g") {nodes {list {name}}}}""" + assert client.query(query_nodes) == { + "graph": { + "nodes": { + "list": [{"name": "ben"}, {"name": "shivam"}] + } + } + } + + create_node_query = """{updateGraph(path: "g") { createNode(time: 0, name: "oogway") { success } }}""" + + assert client.query(create_node_query) == {"updateGraph": {"createNode": {"success": True}}} + assert client.query(query_nodes) == { + "graph": { + "nodes": { + "list": [{"name": "ben"}, {"name": "shivam"}, {"name": "oogway"}] + } + } + } + + with pytest.raises(Exception) as excinfo: + client.query(create_node_query) + + assert "Node already exists" in str(excinfo.value) + + +def test_create_node_using_client(): + g = Graph() + g.add_edge(1, "ben", "shivam") + + tmp_work_dir = tempfile.mkdtemp() + with GraphServer(tmp_work_dir).start(port=1737): + client = RaphtoryClient("http://localhost:1737") + client.send_graph(path="g", graph=g) + + query_nodes = """{graph(path: "g") {nodes {list {name}}}}""" + assert client.query(query_nodes) == { + "graph": { + "nodes": { + "list": [{"name": "ben"}, {"name": "shivam"}] + } + } + } + + remote_graph = client.remote_graph(path="g") + remote_graph.create_node(timestamp=0, id="oogway") + assert client.query(query_nodes) == { + "graph": { + "nodes": { + "list": [{"name": "ben"}, {"name": "shivam"}, {"name": "oogway"}] + } + } + } + + with pytest.raises(Exception) as excinfo: + remote_graph.create_node(timestamp=0, id="oogway") + + assert "Node already exists" in str(excinfo.value) + + +def test_create_node_using_client_with_properties(): + g = Graph() + g.add_edge(1, "ben", "shivam") + + tmp_work_dir = tempfile.mkdtemp() + with GraphServer(tmp_work_dir).start(port=1737): + client = RaphtoryClient("http://localhost:1737") + client.send_graph(path="g", graph=g) + + query_nodes = """{graph(path: "g") {nodes {list {name, properties { keys }}}}}""" + assert client.query(query_nodes) == { + "graph": { + "nodes": { + "list": [{"name": "ben", 'properties': {'keys': []}}, {"name": "shivam", 'properties': {'keys': []}}] + } + } + } + + remote_graph = client.remote_graph(path="g") + remote_graph.create_node(timestamp=0, id="oogway", properties={"prop1": 60, "prop2": 31.3, "prop3": "abc123", "prop4": True, "prop5": [1, 2, 3]}) + nodes = json.loads(json.dumps(client.query(query_nodes)))['graph']['nodes']['list'] + node_oogway = next(node for node in nodes if node['name'] == 'oogway') + assert sorted(node_oogway['properties']['keys']) == ['prop1', 'prop2', 'prop3', 'prop4', 'prop5'] + + with pytest.raises(Exception) as excinfo: + remote_graph.create_node(timestamp=0, id="oogway", properties={"prop1": 60, "prop2": 31.3, "prop3": "abc123", "prop4": True, "prop5": [1, 2, 3]}) + + assert "Node already exists" in str(excinfo.value) + + +def test_create_node_using_client_with_properties_node_type(): + g = Graph() + g.add_edge(1, "ben", "shivam") + + tmp_work_dir = tempfile.mkdtemp() + with GraphServer(tmp_work_dir).start(port=1737): + client = RaphtoryClient("http://localhost:1737") + client.send_graph(path="g", graph=g) + + query_nodes = """{graph(path: "g") {nodes {list {name, nodeType, properties { keys }}}}}""" + assert client.query(query_nodes) == { + "graph": { + "nodes": { + "list": [{"name": "ben", 'nodeType': None, 'properties': {'keys': []}}, {"name": "shivam", 'nodeType': None, 'properties': {'keys': []}}] + } + } + } + + remote_graph = client.remote_graph(path="g") + remote_graph.create_node(timestamp=0, id="oogway", properties={"prop1": 60, "prop2": 31.3, "prop3": "abc123", "prop4": True, "prop5": [1, 2, 3]}, node_type="master") + nodes = json.loads(json.dumps(client.query(query_nodes)))['graph']['nodes']['list'] + node_oogway = next(node for node in nodes if node['name'] == 'oogway') + assert node_oogway['nodeType'] == 'master' + assert sorted(node_oogway['properties']['keys']) == ['prop1', 'prop2', 'prop3', 'prop4', 'prop5'] + + with pytest.raises(Exception) as excinfo: + remote_graph.create_node(timestamp=0, id="oogway", properties={"prop1": 60, "prop2": 31.3, "prop3": "abc123", "prop4": True, "prop5": [1, 2, 3]}, node_type="master") + + assert "Node already exists" in str(excinfo.value) + + +def test_create_node_using_client_with_node_type(): + g = Graph() + g.add_edge(1, "ben", "shivam") + + tmp_work_dir = tempfile.mkdtemp() + with GraphServer(tmp_work_dir).start(port=1737): + client = RaphtoryClient("http://localhost:1737") + client.send_graph(path="g", graph=g) + + query_nodes = """{graph(path: "g") {nodes {list {name, nodeType}}}}""" + assert client.query(query_nodes) == { + "graph": { + "nodes": { + "list": [{"name": "ben", 'nodeType': None}, {"name": "shivam", 'nodeType': None}] + } + } + } + + remote_graph = client.remote_graph(path="g") + remote_graph.create_node(timestamp=0, id="oogway", node_type="master") + assert client.query(query_nodes) == { + "graph": { + "nodes": { + "list": [{"name": "ben", 'nodeType': None}, {"name": "shivam", 'nodeType': None}, {"name": "oogway", 'nodeType': "master"}] + } + } + } + + with pytest.raises(Exception) as excinfo: + remote_graph.create_node(timestamp=0, id="oogway", node_type="master") + + assert "Node already exists" in str(excinfo.value) + + # def test_disk_graph_name(): # import pandas as pd # from raphtory import DiskGraphStorage diff --git a/python/tests/test_graphdb/test_graphdb.py b/python/tests/test_graphdb/test_graphdb.py index f4dc42bf3..eb1c43b6b 100644 --- a/python/tests/test_graphdb/test_graphdb.py +++ b/python/tests/test_graphdb/test_graphdb.py @@ -353,8 +353,8 @@ def test_getitem(): @with_disk_graph def check(g): assert ( - g.node(1).properties.temporal.get("cost") - == g.node(1).properties.temporal["cost"] + g.node(1).properties.temporal.get("cost") + == g.node(1).properties.temporal["cost"] ) check(g) @@ -607,7 +607,7 @@ def time_history_test(time, key, value): assert g.at(time).node(1).properties.temporal.get(key) is None assert g.at(time).nodes.properties.temporal.get(key) is None assert ( - g.at(time).nodes.out_neighbours.properties.temporal.get(key) is None + g.at(time).nodes.out_neighbours.properties.temporal.get(key) is None ) else: assert g.at(time).node(1).properties.temporal.get(key).items() == value @@ -812,22 +812,22 @@ def no_static_property_test(key, value): assert sorted(g.node(1).properties.temporal.keys()) == expected_names_no_static assert sorted(g.nodes.properties.temporal.keys()) == expected_names_no_static assert ( - sorted(g.nodes.out_neighbours.properties.temporal.keys()) - == expected_names_no_static + sorted(g.nodes.out_neighbours.properties.temporal.keys()) + == expected_names_no_static ) expected_names_no_static_at_1 = sorted(["prop 4", "prop 1", "prop 3"]) assert ( - sorted(g.at(1).node(1).properties.temporal.keys()) - == expected_names_no_static_at_1 + sorted(g.at(1).node(1).properties.temporal.keys()) + == expected_names_no_static_at_1 ) assert ( - sorted(g.at(1).nodes.properties.temporal.keys()) - == expected_names_no_static_at_1 + sorted(g.at(1).nodes.properties.temporal.keys()) + == expected_names_no_static_at_1 ) assert ( - sorted(g.at(1).nodes.out_neighbours.properties.temporal.keys()) - == expected_names_no_static_at_1 + sorted(g.at(1).nodes.out_neighbours.properties.temporal.keys()) + == expected_names_no_static_at_1 ) # testing has_property @@ -1325,11 +1325,11 @@ def test_constant_property_update(): def updates(v): v.update_constant_properties({"prop1": "value1", "prop2": 123}) assert ( - v.properties.get("prop1") == "value1" and v.properties.get("prop2") == 123 + v.properties.get("prop1") == "value1" and v.properties.get("prop2") == 123 ) v.update_constant_properties({"prop1": "value2", "prop2": 345}) assert ( - v.properties.get("prop1") == "value2" and v.properties.get("prop2") == 345 + v.properties.get("prop1") == "value2" and v.properties.get("prop2") == 345 ) v.add_constant_properties({"name": "value1"}) @@ -1666,18 +1666,18 @@ def check(g): assert g.exclude_layer("layer2").count_edges() == 4 with pytest.raises( - Exception, - match=re.escape( - "Invalid layer: test_layer. Valid layers: _default, layer1, layer2" - ), + Exception, + match=re.escape( + "Invalid layer: test_layer. Valid layers: _default, layer1, layer2" + ), ): g.layers(["test_layer"]) with pytest.raises( - Exception, - match=re.escape( - "Invalid layer: test_layer. Valid layers: _default, layer1, layer2" - ), + Exception, + match=re.escape( + "Invalid layer: test_layer. Valid layers: _default, layer1, layer2" + ), ): g.edge(1, 2).layers(["test_layer"]) @@ -1754,20 +1754,20 @@ def test_layer_name(): assert str(e.value) == error_msg assert [ - list(iterator) for iterator in g.nodes.neighbours.edges.explode().layer_name - ] == [ - ["_default", "awesome layer"], - ["_default", "awesome layer"], - ["_default", "awesome layer"], - ] + list(iterator) for iterator in g.nodes.neighbours.edges.explode().layer_name + ] == [ + ["_default", "awesome layer"], + ["_default", "awesome layer"], + ["_default", "awesome layer"], + ] assert [ - list(iterator) - for iterator in g.nodes.neighbours.edges.explode_layers().layer_name - ] == [ - ["_default", "awesome layer"], - ["_default", "awesome layer"], - ["_default", "awesome layer"], - ] + list(iterator) + for iterator in g.nodes.neighbours.edges.explode_layers().layer_name + ] == [ + ["_default", "awesome layer"], + ["_default", "awesome layer"], + ["_default", "awesome layer"], + ] def test_time(): @@ -1801,12 +1801,12 @@ def check(g): # assert str(e.value) == error_msg assert [ - list(iterator) for iterator in g.nodes.neighbours.edges.explode().time - ] == [ - [0, 0, 1], - [0, 0, 1], - [0, 0, 1], - ] + list(iterator) for iterator in g.nodes.neighbours.edges.explode().time + ] == [ + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + ] check(g) @@ -2371,8 +2371,8 @@ def test_weird_windows(): @with_disk_graph def check(g): with pytest.raises( - Exception, - match="'ddd' is not a valid datetime, valid formats are RFC3339, RFC2822, %Y-%m-%d, %Y-%m-%dT%H:%M:%S%.3f, %Y-%m-%dT%H:%M:%S%, %Y-%m-%d %H:%M:%S%.3f and %Y-%m-%d %H:%M:%S%", + Exception, + match="'ddd' is not a valid datetime, valid formats are RFC3339, RFC2822, %Y-%m-%d, %Y-%m-%dT%H:%M:%S%.3f, %Y-%m-%dT%H:%M:%S%, %Y-%m-%d %H:%M:%S%.3f and %Y-%m-%d %H:%M:%S%", ): g.window("ddd", "aaa") @@ -2569,9 +2569,9 @@ def check(g): assert g.nodes.type_filter(["a"]).neighbours.type_filter( ["c"] ).name.collect() == [ - [], - ["5"], - ] + [], + ["5"], + ] assert g.nodes.type_filter(["a"]).neighbours.type_filter([]).name.collect() == [ [], [], @@ -2582,9 +2582,9 @@ def check(g): assert g.nodes.type_filter(["a"]).neighbours.type_filter( ["d"] ).name.collect() == [ - [], - [], - ] + [], + [], + ] assert g.nodes.type_filter(["a"]).neighbours.neighbours.name.collect() == [ ["1", "3", "4"], ["1", "3", "4", "4", "6"], @@ -2639,20 +2639,20 @@ def check(g): for edge in edges: time_nested.append(edge.time) assert [ - item - for sublist in g.nodes.edges.explode().time.collect() - for item in sublist - ] == time_nested + item + for sublist in g.nodes.edges.explode().time.collect() + for item in sublist + ] == time_nested date_time_nested = [] for edges in g.nodes.edges.explode(): for edge in edges: date_time_nested.append(edge.date_time) assert [ - item - for sublist in g.nodes.edges.explode().date_time.collect() - for item in sublist - ] == date_time_nested + item + for sublist in g.nodes.edges.explode().date_time.collect() + for item in sublist + ] == date_time_nested check(g) @@ -2966,49 +2966,75 @@ def check(g): assert len(index.fuzzy_search_nodes("name:habza", levenshtein_distance=1)) == 1 assert ( - len( - index.fuzzy_search_nodes( - "name:haa", levenshtein_distance=1, prefix=True + len( + index.fuzzy_search_nodes( + "name:haa", levenshtein_distance=1, prefix=True + ) ) - ) - == 2 + == 2 ) assert ( - len( - index.fuzzy_search_nodes( - "value_str:abc123", levenshtein_distance=2, prefix=True + len( + index.fuzzy_search_nodes( + "value_str:abc123", levenshtein_distance=2, prefix=True + ) ) - ) - == 2 + == 2 ) assert ( - len( - index.fuzzy_search_nodes( - "value_str:dsss312", levenshtein_distance=2, prefix=False + len( + index.fuzzy_search_nodes( + "value_str:dsss312", levenshtein_distance=2, prefix=False + ) ) - ) - == 1 + == 1 ) assert len(index.fuzzy_search_edges("from:bon", levenshtein_distance=1)) == 2 assert ( - len( - index.fuzzy_search_edges("from:bo", levenshtein_distance=1, prefix=True) - ) - == 2 + len( + index.fuzzy_search_edges("from:bo", levenshtein_distance=1, prefix=True) + ) + == 2 ) assert ( - len( - index.fuzzy_search_edges( - "from:eon", levenshtein_distance=2, prefix=True + len( + index.fuzzy_search_edges( + "from:eon", levenshtein_distance=2, prefix=True + ) ) - ) - == 2 + == 2 ) check(g) +def test_create_node_graph(): + g = Graph() + g.create_node(1, "shivam", properties={"value": 60, "value_f": 31.3, "value_str": "abc123"}) + node = g.node("shivam") + assert node.name == "shivam" + assert node.properties == {"value": 60, "value_f": 31.3, "value_str": "abc123"} + + with pytest.raises(Exception) as excinfo: + g.create_node(1, "shivam", properties={"value": 60, "value_f": 31.3, "value_str": "abc123"}) + + assert "Node already exists" in str(excinfo.value) + + +def test_create_node_graph_with_deletion(): + g = PersistentGraph() + g.create_node(1, "shivam", properties={"value": 60, "value_f": 31.3, "value_str": "abc123"}) + node = g.node("shivam") + assert node.name == "shivam" + assert node.properties == {"value": 60, "value_f": 31.3, "value_str": "abc123"} + + with pytest.raises(Exception) as excinfo: + g.create_node(1, "shivam", properties={"value": 60, "value_f": 31.3, "value_str": "abc123"}) + + assert "Node already exists" in str(excinfo.value) + + @fixture def datadir(tmpdir, request): filename = request.module.__file__ @@ -3021,7 +3047,6 @@ def datadir(tmpdir, request): raise e return tmpdir - # def currently_broken_fuzzy_search(): #TODO: Fix fuzzy searching for properties # g = Graph() # g.add_edge(2,"haaroon","hamza", properties={"value":60,"value_f":31.3,"value_str":"abc123"}) diff --git a/raphtory-graphql/src/model/graph/mutable_graph.rs b/raphtory-graphql/src/model/graph/mutable_graph.rs index ff1e00208..fb991316b 100644 --- a/raphtory-graphql/src/model/graph/mutable_graph.rs +++ b/raphtory-graphql/src/model/graph/mutable_graph.rs @@ -96,6 +96,25 @@ impl GqlMutableGraph { Ok(node.into()) } + /// Create a new node or fail if it already exists + async fn create_node( + &self, + time: i64, + name: String, + properties: Option>, + node_type: Option, + ) -> Result { + let node = self.graph.create_node( + time, + &name, + as_properties(properties.unwrap_or(vec![])), + node_type.as_str(), + )?; + node.update_embeddings().await?; + self.graph.write_updates()?; + Ok(node.into()) + } + /// Add a batch of nodes async fn add_nodes(&self, nodes: Vec) -> Result { for node in nodes { diff --git a/raphtory-graphql/src/python/client/raphtory_client.rs b/raphtory-graphql/src/python/client/raphtory_client.rs index ca40ef06d..23bc023e5 100644 --- a/raphtory-graphql/src/python/client/raphtory_client.rs +++ b/raphtory-graphql/src/python/client/raphtory_client.rs @@ -125,7 +125,7 @@ impl PyRaphtoryClient { /// /// Returns: /// The `data` field from the graphQL response. - #[pyo3(signature = (query, variables=None))] + #[pyo3(signature = (query, variables = None))] pub(crate) fn query( &self, py: Python, @@ -427,6 +427,9 @@ impl PyRaphtoryClient { /// RemoteGraph /// fn remote_graph(&self, path: String) -> PyRemoteGraph { - PyRemoteGraph::new(path, self.clone()) + PyRemoteGraph { + path, + client: self.clone(), + } } } diff --git a/raphtory-graphql/src/python/client/remote_graph.rs b/raphtory-graphql/src/python/client/remote_graph.rs index 7633ee74a..5c759df14 100644 --- a/raphtory-graphql/src/python/client/remote_graph.rs +++ b/raphtory-graphql/src/python/client/remote_graph.rs @@ -23,11 +23,6 @@ pub struct PyRemoteGraph { #[pymethods] impl PyRemoteGraph { - #[new] - pub(crate) fn new(path: String, client: PyRaphtoryClient) -> Self { - Self { path, client } - } - /// Gets a remote node with the specified id /// /// Arguments: @@ -238,6 +233,52 @@ impl PyRemoteGraph { )) } + /// Create a new node with the given id and properties to the remote graph and fail if the node already exists. + /// + /// Arguments: + /// timestamp (int|str|datetime): The timestamp of the node. + /// id (str|int): The id of the node. + /// properties (dict, optional): The properties of the node. + /// node_type (str, optional): The optional string which will be used as a node type + /// Returns: + /// RemoteNode + #[pyo3(signature = (timestamp, id, properties = None, node_type = None))] + pub fn create_node( + &self, + py: Python, + timestamp: PyTime, + id: GID, + properties: Option>, + node_type: Option<&str>, + ) -> Result { + let template = r#" + { + updateGraph(path: "{{ path }}") { + createNode(time: {{ time }}, name: "{{ name }}" {% if properties is not none %}, properties: {{ properties | safe }} {% endif %}{% if node_type is not none %}, nodeType: "{{ node_type }}"{% endif %}) { + success + } + } + } + "#; + + let query_context = context! { + path => self.path, + time => timestamp.into_time(), + name => id.to_string(), + properties => properties.map(|p| build_property_string(p)), + node_type => node_type + }; + + let query = build_query(template, query_context)?; + let _ = &self.client.query(py, query, None)?; + + Ok(PyRemoteNode::new( + self.path.clone(), + self.client.clone(), + id.to_string(), + )) + } + /// Adds properties to the remote graph. /// /// Arguments: diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 6068fab44..fa3e91005 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -12,7 +12,9 @@ use crate::{ }, graph::{edge::EdgeView, node::NodeView}, }, + prelude::NodeViewOps, }; +use raphtory_api::core::storage::dict_mapper::MaybeNew::{Existing, New}; pub trait AdditionOps: StaticGraphViewOps { // TODO: Probably add vector reference here like add @@ -45,6 +47,14 @@ pub trait AdditionOps: StaticGraphViewOps { node_type: Option<&str>, ) -> Result, GraphError>; + fn create_node( + &self, + t: T, + v: V, + props: PI, + node_type: Option<&str>, + ) -> Result, GraphError>; + fn add_node_with_custom_time_format( &self, t: &str, @@ -123,6 +133,36 @@ impl AdditionOps for G { Ok(NodeView::new_internal(self.clone(), v_id)) } + fn create_node( + &self, + t: T, + v: V, + props: PI, + node_type: Option<&str>, + ) -> Result, GraphError> { + let ti = time_from_input(self, t)?; + let v_id = match node_type { + None => self.resolve_node(v)?, + Some(node_type) => { + let (v_id, _) = self.resolve_node_and_type(v, node_type)?.inner(); + v_id + } + }; + match v_id { + New(id) => { + let properties = props.collect_properties(|name, dtype| { + Ok(self.resolve_node_property(name, dtype, false)?.inner()) + })?; + self.internal_add_node(ti, id, &properties)?; + Ok(NodeView::new_internal(self.clone(), id)) + } + Existing(id) => { + let node_id = self.node(id).unwrap().id(); + Err(GraphError::NodeExistsError(node_id)) + } + } + } + fn add_edge( &self, t: T, diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 0b07cd0a1..49f83120e 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -3235,4 +3235,19 @@ mod db_tests { let graph = pool.install(|| Graph::new()); assert_eq!(graph.core_graph().internal_num_nodes(), 0); } + + #[test] + fn test_create_node() { + let g = Graph::new(); + g.create_node(0, 1, [("test", Prop::Bool(true))], None) + .unwrap(); + + let n = g.node(1).unwrap(); + + assert_eq!(n.id().as_u64().unwrap(), 1); + assert_eq!(n.properties().get("test").unwrap(), Prop::Bool(true)); + + let result = g.create_node(1, 1, [("test".to_string(), Prop::Bool(true))], None); + assert!(matches!(result, Err(GraphError::NodeExistsError(id)) if id == GID::U64(1))); + } } diff --git a/raphtory/src/python/graph/graph.rs b/raphtory/src/python/graph/graph.rs index 97287c1be..5f4849017 100644 --- a/raphtory/src/python/graph/graph.rs +++ b/raphtory/src/python/graph/graph.rs @@ -190,6 +190,27 @@ impl PyGraph { .add_node(timestamp, id, properties.unwrap_or_default(), node_type) } + /// Creates a new node with the given id and properties to the graph. It fails if the node already exists. + /// + /// Arguments: + /// timestamp (TimeInput): The timestamp of the node. + /// 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 + #[pyo3(signature = (timestamp, id, properties = None, node_type = None))] + pub fn create_node( + &self, + timestamp: PyTime, + id: GID, + properties: Option>, + node_type: Option<&str>, + ) -> Result, GraphError> { + self.graph + .create_node(timestamp, id, properties.unwrap_or_default(), node_type) + } + /// Adds properties to the graph. /// /// Arguments: diff --git a/raphtory/src/python/graph/graph_with_deletions.rs b/raphtory/src/python/graph/graph_with_deletions.rs index f4650875b..e3f78469b 100644 --- a/raphtory/src/python/graph/graph_with_deletions.rs +++ b/raphtory/src/python/graph/graph_with_deletions.rs @@ -131,6 +131,28 @@ impl PyPersistentGraph { .add_node(timestamp, id, properties.unwrap_or_default(), node_type) } + /// Creates a new node with the given id and properties to the graph. It fails if the node already exists. + /// + /// Arguments: + /// timestamp (TimeInput): The timestamp of the node. + /// id (str | int): The id of the node. + /// properties (dict): The properties of the node. + /// node_type (str) : The optional string which will be used as a node type + /// + /// Returns: + /// MutableNode + #[pyo3(signature = (timestamp, id, properties = None, node_type = None))] + pub fn create_node( + &self, + timestamp: PyTime, + id: GID, + properties: Option>, + node_type: Option<&str>, + ) -> Result, GraphError> { + self.graph + .create_node(timestamp, id, properties.unwrap_or_default(), node_type) + } + /// Adds properties to the graph. /// /// Arguments: