Repository: Pometry/Raphtory (language: python)

Issue:
impl filters and tests
### What changes were proposed in this pull request?

### Why are the changes needed?

### Does this PR introduce any user-facing change? If yes is this documented?

### How was this patch tested?

### Issues

_If this resolves any issues, please link to them here, the format is a KEYWORD followed by @<ISSUE NUMBER>__
KEYWORDS available are `close`, `closes`, `closed`, `fix`, `fixes`, `fixed`, `resolve`, `resolves`, `resolved`.
Please delete this text before creating your PR 

### Are there any further changes required?





Discussion / hints:
Initial ideas on what shapes these apis will be taking going forward alongside developments in lib.
```graphql
query w {
  graph(path: "mygraph1", nodeFilter: {}, edgeFilter: {}) {
    # nodeFilter() {}
    # edgeFitler() {}
    
    nodes(nodeFilter: {
      or: [
        {and: [{prop: {field: "a", ge: 1}}, {type: ["a","b","c"]}]},
        {prop: {field: "b", any: [1,2,3]}}
      ]
    }) {
      list {
        name
      }
  	}
	}
}

query w2 {
  graph(path: "mygraph1") {
    # nodePropertyFilter() {}
    # edgePropertyFitler() {}
    
    n1: nodes(nodeFilter: {
      and: [{propFilter: {field: "a", ge: 1}}, {typeFilter: ["a","b","c"]}]
    }) {
      list {
        name
      }
  	}
    
    n2: nodes(nodeFilter: {
      propFilter: {field: "a", ge: 1}
    }) {
      list {
        name
      }
  	}
	}
}
```

Fix patch:
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<GID>),
+
     #[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<GHH, GH>,
-        force: bool,
+        merge: bool,
     ) -> Result<NodeView<Self, Self>, 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<GHH, GH>,
+        new_id: V,
+        merge: bool,
+    ) -> Result<NodeView<Self, Self>, 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<Item = impl Borrow<NodeView<GHH, GH>>>,
-        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<Item = impl Borrow<NodeView<GHH, GH>>>,
+        new_ids: impl IntoIterator<Item = V>,
+        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<GHH, GH>,
-        force: bool,
+        merge: bool,
     ) -> Result<EdgeView<Self, Self>, 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<GHH, GH>,
+        new_id: (V, V),
+        merge: bool,
+    ) -> Result<EdgeView<Self, Self>, 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<Item = impl Borrow<EdgeView<GHH, GH>>>,
-        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<Item = impl Borrow<EdgeView<GHH, GH>>>,
+        new_ids: impl IntoIterator<Item = (V, V)>,
+        merge: bool,
     ) -> Result<(), GraphError>;
 }
 
@@ -122,56 +218,54 @@ impl<
     fn import_node<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>(
         &self,
         node: &NodeView<GHH, GH>,
-        force: bool,
+        merge: bool,
     ) -> Result<NodeView<G, G>, 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<GHH, GH>,
+        new_id: V,
+        merge: bool,
+    ) -> Result<NodeView<Self, Self>, GraphError> {
+        import_node_internal(&self, node, new_id, merge)
     }
 
     fn import_nodes<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>(
         &self,
         nodes: impl IntoIterator<Item = impl Borrow<NodeView<GHH, GH>>>,
-        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<GID> = 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<Item = impl Borrow<NodeView<GHH, GH>>>,
+        new_ids: impl IntoIterator<Item = V>,
+        merge: bool,
+    ) -> Result<(), GraphError> {
+        let new_ids: Vec<V> = 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<GHH, GH>,
-        force: bool,
+        merge: bool,
     ) -> Result<EdgeView<Self, Self>, 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<GHH, GH>,
+        new_id: (V, V),
+        merge: bool,
+    ) -> Result<EdgeView<Self, Self>, 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<Item = impl Borrow<EdgeView<GHH, GH>>>,
-        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<Item = impl Borrow<EdgeView<GHH, GH>>>,
+        new_ids: impl IntoIterator<Item = (V, V)>,
+        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<GHH, GH>,
+    id: V,
+    merge: bool,
+) -> Result<NodeView<G, G>, 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<GHH, GH>,
+    src_id: V,
+    dst_id: V,
+    merge: bool,
+) -> Result<EdgeView<G, G>, 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<String>>(),
+                    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<(String, String)>>(),
+                    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<String>>(),
+                    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<(String, String)>>(),
+                    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<String, Prop>,
@@ -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<String, Prop>,
@@ -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<NodeView<Graph, Graph>, 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<NodeView<Graph, Graph>, 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<PyNode>, 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<PyNode>, 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<PyNode>,
+        new_ids: Vec<GID>,
+        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<EdgeView<Graph, Graph>, 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<EdgeView<Graph, Graph>, 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<PyEdge>, 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<PyEdge>, 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<PyEdge>,
+        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<NodeView<Graph>> {
         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<String, Prop>,
@@ -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<String, Prop>,
@@ -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<NodeView<PersistentGraph>> {
         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<NodeView<Graph, Graph>, 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<NodeView<PersistentGraph, PersistentGraph>, 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<NodeView<PersistentGraph, PersistentGraph>, 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<PyNode>, force: bool) -> Result<(), GraphError> {
+    /// Raises:
+    ///     GraphError: If the operation fails.
+    #[pyo3(signature = (nodes, merge = false))]
+    pub fn import_nodes(&self, nodes: Vec<PyNode>, 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<PyNode>,
+        new_ids: Vec<GID>,
+        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<EdgeView<PersistentGraph, PersistentGraph>, 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<EdgeView<PersistentGraph, PersistentGraph>, 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<PyEdge>, force: bool) -> Result<(), GraphError> {
+    /// Raises:
+    ///     GraphError: If the operation fails.
+    #[pyo3(signature = (edges, merge = false))]
+    pub fn import_edges(&self, edges: Vec<PyEdge>, 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<PyEdge>,
+        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.


Test patch:
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)