kitedb 0.2.10

High-performance embedded graph database
Documentation
//! Node operations for Python bindings

use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;

use crate::core::single_file::SingleFileDB as RustSingleFileDB;
use crate::graph::db::GraphDB as RustGraphDB;
use crate::graph::iterators::{count_nodes as graph_count_nodes, list_nodes as graph_list_nodes};
use crate::graph::key_index::get_node_key as graph_get_node_key;
use crate::graph::nodes::{
  create_node as graph_create_node, delete_node as graph_delete_node, get_node_by_key_db,
  node_exists_db, upsert_node_by_id_with_props, upsert_node_with_props, NodeOpts,
};
use crate::graph::tx::TxHandle as GraphTxHandle;
use crate::types::{NodeId, PropKeyId, PropValue};

/// Trait for node operations
pub trait NodeOps {
  /// Create a new node
  fn create_node_impl(&self, key: Option<String>) -> PyResult<i64>;

  /// Delete a node
  fn delete_node_impl(&self, node_id: i64) -> PyResult<()>;

  /// Check if a node exists
  fn node_exists_impl(&self, node_id: i64) -> PyResult<bool>;

  /// Get node by key
  fn get_node_by_key_impl(&self, key: &str) -> PyResult<Option<i64>>;

  /// Get the key for a node
  fn get_node_key_impl(&self, node_id: i64) -> PyResult<Option<String>>;

  /// List all node IDs
  fn list_nodes_impl(&self) -> PyResult<Vec<i64>>;

  /// Count all nodes
  fn count_nodes_impl(&self) -> PyResult<i64>;
}

// ============================================================================
// Single-file database operations
// ============================================================================

/// Create node on single-file database
pub fn create_node_single(db: &RustSingleFileDB, key: Option<&str>) -> PyResult<i64> {
  let node_id = db
    .create_node(key)
    .map_err(|e| PyRuntimeError::new_err(format!("Failed to create node: {e}")))?;
  Ok(node_id as i64)
}

/// Delete node on single-file database
pub fn delete_node_single(db: &RustSingleFileDB, node_id: NodeId) -> PyResult<()> {
  db.delete_node(node_id)
    .map_err(|e| PyRuntimeError::new_err(format!("Failed to delete node: {e}")))
}

/// Check node exists on single-file database
pub fn node_exists_single(db: &RustSingleFileDB, node_id: NodeId) -> bool {
  db.node_exists(node_id)
}

/// Get node by key on single-file database
pub fn get_node_by_key_single(db: &RustSingleFileDB, key: &str) -> Option<i64> {
  db.get_node_by_key(key).map(|id| id as i64)
}

/// Get node key on single-file database
pub fn get_node_key_single(db: &RustSingleFileDB, node_id: NodeId) -> Option<String> {
  db.get_node_key(node_id)
}

/// List nodes on single-file database
pub fn list_nodes_single(db: &RustSingleFileDB) -> Vec<i64> {
  db.list_nodes().into_iter().map(|id| id as i64).collect()
}

/// Count nodes on single-file database
pub fn count_nodes_single(db: &RustSingleFileDB) -> i64 {
  db.count_nodes() as i64
}

// ============================================================================
// Graph database operations
// ============================================================================

/// Create node on graph database (requires transaction handle)
pub fn create_node_graph(handle: &mut GraphTxHandle, key: Option<String>) -> PyResult<i64> {
  let mut opts = NodeOpts::new();
  if let Some(k) = key {
    opts = opts.with_key(k);
  }
  let node_id = graph_create_node(handle, opts)
    .map_err(|e| PyRuntimeError::new_err(format!("Failed to create node: {e}")))?;
  Ok(node_id as i64)
}

/// Upsert node on single-file database
pub fn upsert_node_single(
  db: &RustSingleFileDB,
  key: &str,
  props: &[(PropKeyId, Option<PropValue>)],
) -> PyResult<i64> {
  let node_id = match db.get_node_by_key(key) {
    Some(id) => id,
    None => db
      .create_node(Some(key))
      .map_err(|e| PyRuntimeError::new_err(format!("Failed to create node: {e}")))?,
  };

  for (prop_key_id, value_opt) in props {
    match value_opt {
      Some(value) => db
        .set_node_prop(node_id, *prop_key_id, value.clone())
        .map_err(|e| PyRuntimeError::new_err(format!("Failed to set prop: {e}")))?,
      None => db
        .delete_node_prop(node_id, *prop_key_id)
        .map_err(|e| PyRuntimeError::new_err(format!("Failed to delete prop: {e}")))?,
    }
  }

  Ok(node_id as i64)
}

/// Upsert node on graph database (requires transaction handle)
pub fn upsert_node_graph(
  handle: &mut GraphTxHandle,
  key: &str,
  props: &[(PropKeyId, Option<PropValue>)],
) -> PyResult<i64> {
  let (node_id, _) = upsert_node_with_props(handle, key, props.iter().cloned())
    .map_err(|e| PyRuntimeError::new_err(format!("Failed to upsert node: {e}")))?;
  Ok(node_id as i64)
}

/// Upsert node by ID on single-file database
pub fn upsert_node_by_id_single(
  db: &RustSingleFileDB,
  node_id: NodeId,
  props: &[(PropKeyId, Option<PropValue>)],
) -> PyResult<i64> {
  if !db.node_exists(node_id) {
    db.create_node_with_id(node_id, None)
      .map_err(|e| PyRuntimeError::new_err(format!("Failed to create node: {e}")))?;
  }

  for (prop_key_id, value_opt) in props {
    match value_opt {
      Some(value) => db
        .set_node_prop(node_id, *prop_key_id, value.clone())
        .map_err(|e| PyRuntimeError::new_err(format!("Failed to set prop: {e}")))?,
      None => db
        .delete_node_prop(node_id, *prop_key_id)
        .map_err(|e| PyRuntimeError::new_err(format!("Failed to delete prop: {e}")))?,
    }
  }

  Ok(node_id as i64)
}

/// Upsert node by ID on graph database (requires transaction handle)
pub fn upsert_node_by_id_graph(
  handle: &mut GraphTxHandle,
  node_id: NodeId,
  props: &[(PropKeyId, Option<PropValue>)],
) -> PyResult<i64> {
  let opts = NodeOpts::new();
  let (node_id, _) = upsert_node_by_id_with_props(handle, node_id, opts, props.iter().cloned())
    .map_err(|e| PyRuntimeError::new_err(format!("Failed to upsert node: {e}")))?;
  Ok(node_id as i64)
}

/// Delete node on graph database (requires transaction handle)
pub fn delete_node_graph(handle: &mut GraphTxHandle, node_id: NodeId) -> PyResult<()> {
  graph_delete_node(handle, node_id)
    .map_err(|e| PyRuntimeError::new_err(format!("Failed to delete node: {e}")))?;
  Ok(())
}

/// Check node exists on graph database
pub fn node_exists_graph(db: &RustGraphDB, node_id: NodeId) -> bool {
  node_exists_db(db, node_id)
}

/// Get node by key on graph database
pub fn get_node_by_key_graph(db: &RustGraphDB, key: &str) -> Option<i64> {
  get_node_by_key_db(db, key).map(|id| id as i64)
}

/// Get node key on graph database
pub fn get_node_key_graph(db: &RustGraphDB, node_id: NodeId) -> Option<String> {
  let delta = db.delta.read();
  graph_get_node_key(db.snapshot.as_ref(), &delta, node_id)
}

/// List nodes on graph database
pub fn list_nodes_graph(db: &RustGraphDB) -> Vec<i64> {
  graph_list_nodes(db)
    .into_iter()
    .map(|id| id as i64)
    .collect()
}

/// Count nodes on graph database
pub fn count_nodes_graph(db: &RustGraphDB) -> i64 {
  graph_count_nodes(db) as i64
}

/// List nodes with key prefix on single-file database
pub fn list_nodes_with_prefix_single(db: &RustSingleFileDB, prefix: &str) -> Vec<i64> {
  db.list_nodes()
    .into_iter()
    .filter(|&id| {
      if let Some(key) = db.get_node_key(id) {
        key.starts_with(prefix)
      } else {
        false
      }
    })
    .map(|id| id as i64)
    .collect()
}

/// List nodes with key prefix on graph database
pub fn list_nodes_with_prefix_graph(db: &RustGraphDB, prefix: &str) -> Vec<i64> {
  graph_list_nodes(db)
    .into_iter()
    .filter(|&id| {
      if let Some(key) = get_node_key_graph(db, id) {
        key.starts_with(prefix)
      } else {
        false
      }
    })
    .map(|id| id as i64)
    .collect()
}

/// Count nodes with key prefix on single-file database
pub fn count_nodes_with_prefix_single(db: &RustSingleFileDB, prefix: &str) -> i64 {
  db.list_nodes()
    .into_iter()
    .filter(|&id| {
      if let Some(key) = db.get_node_key(id) {
        key.starts_with(prefix)
      } else {
        false
      }
    })
    .count() as i64
}

/// Count nodes with key prefix on graph database
pub fn count_nodes_with_prefix_graph(db: &RustGraphDB, prefix: &str) -> i64 {
  graph_list_nodes(db)
    .into_iter()
    .filter(|&id| {
      if let Some(key) = get_node_key_graph(db, id) {
        key.starts_with(prefix)
      } else {
        false
      }
    })
    .count() as i64
}

#[cfg(test)]
mod tests {
  // Node operation tests require database instances
  // Better tested through integration tests
}