use crate::error::{RayError, Result};
use crate::graph::db::{close_graph_db, open_graph_db, GraphDB, OpenOptions};
use crate::graph::edges::{
add_edge,
del_edge_prop,
delete_edge,
edge_exists,
edge_exists_db,
get_edge_prop_db,
get_edge_props_db,
get_neighbors_in_db,
get_neighbors_out_db,
set_edge_prop,
};
use crate::graph::iterators::{
count_edges, count_nodes, list_edges, list_nodes, FullEdge, ListEdgesOptions,
};
use crate::graph::key_index::get_node_key;
use crate::graph::nodes::{
create_node, del_node_prop, delete_node, get_node_by_key, get_node_by_key_db, get_node_prop,
get_node_prop_db, node_exists, node_exists_db, set_node_prop, NodeOpts,
};
use crate::graph::tx::{begin_tx, commit, rollback, TxHandle};
use crate::types::*;
use std::collections::{HashMap, HashSet};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct PropDef {
pub name: String,
pub prop_type: PropType,
pub required: bool,
pub default: Option<PropValue>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropType {
String,
Int,
Float,
Bool,
Any,
}
impl PropDef {
pub fn string(name: &str) -> Self {
Self {
name: name.to_string(),
prop_type: PropType::String,
required: false,
default: None,
}
}
pub fn int(name: &str) -> Self {
Self {
name: name.to_string(),
prop_type: PropType::Int,
required: false,
default: None,
}
}
pub fn float(name: &str) -> Self {
Self {
name: name.to_string(),
prop_type: PropType::Float,
required: false,
default: None,
}
}
pub fn bool(name: &str) -> Self {
Self {
name: name.to_string(),
prop_type: PropType::Bool,
required: false,
default: None,
}
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn default(mut self, value: PropValue) -> Self {
self.default = Some(value);
self
}
}
#[derive(Debug, Clone)]
pub struct NodeDef {
pub name: String,
pub props: HashMap<String, PropDef>,
pub key_prefix: String,
pub label_id: Option<LabelId>,
pub prop_key_ids: HashMap<String, PropKeyId>,
}
impl NodeDef {
pub fn new(name: &str, key_prefix: &str) -> Self {
Self {
name: name.to_string(),
props: HashMap::new(),
key_prefix: key_prefix.to_string(),
label_id: None,
prop_key_ids: HashMap::new(),
}
}
pub fn prop(mut self, prop: PropDef) -> Self {
self.props.insert(prop.name.clone(), prop);
self
}
pub fn key(&self, suffix: &str) -> String {
format!("{}{}", self.key_prefix, suffix)
}
}
#[derive(Debug, Clone)]
pub struct EdgeDef {
pub name: String,
pub props: HashMap<String, PropDef>,
pub etype_id: Option<ETypeId>,
pub prop_key_ids: HashMap<String, PropKeyId>,
}
impl EdgeDef {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
props: HashMap::new(),
etype_id: None,
prop_key_ids: HashMap::new(),
}
}
pub fn prop(mut self, prop: PropDef) -> Self {
self.props.insert(prop.name.clone(), prop);
self
}
}
#[derive(Debug, Clone)]
pub struct NodeRef {
pub id: NodeId,
pub key: Option<String>,
pub node_type: String,
}
impl NodeRef {
pub fn new(id: NodeId, key: Option<String>, node_type: &str) -> Self {
Self {
id,
key,
node_type: node_type.to_string(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RayOptions {
pub nodes: Vec<NodeDef>,
pub edges: Vec<EdgeDef>,
pub read_only: bool,
pub create_if_missing: bool,
pub lock_file: bool,
}
impl RayOptions {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
read_only: false,
create_if_missing: true,
lock_file: true,
}
}
pub fn node(mut self, node: NodeDef) -> Self {
self.nodes.push(node);
self
}
pub fn edge(mut self, edge: EdgeDef) -> Self {
self.edges.push(edge);
self
}
pub fn read_only(mut self, value: bool) -> Self {
self.read_only = value;
self
}
}
pub struct Ray {
db: GraphDB,
nodes: HashMap<String, NodeDef>,
edges: HashMap<String, EdgeDef>,
key_prefix_to_node: HashMap<String, String>,
}
impl Ray {
pub fn open<P: AsRef<Path>>(path: P, options: RayOptions) -> Result<Self> {
let db_options = OpenOptions {
read_only: options.read_only,
create_if_missing: options.create_if_missing,
lock_file: options.lock_file,
..Default::default()
};
let db = open_graph_db(path, db_options)?;
let mut nodes: HashMap<String, NodeDef> = HashMap::new();
let mut edges: HashMap<String, EdgeDef> = HashMap::new();
let mut key_prefix_to_node: HashMap<String, String> = HashMap::new();
for mut node_def in options.nodes {
let label_id = db.get_or_create_label(&node_def.name);
node_def.label_id = Some(label_id);
for prop_name in node_def.props.keys() {
let prop_key_id = db.get_or_create_propkey(prop_name);
node_def.prop_key_ids.insert(prop_name.clone(), prop_key_id);
}
key_prefix_to_node.insert(node_def.key_prefix.clone(), node_def.name.clone());
nodes.insert(node_def.name.clone(), node_def);
}
for mut edge_def in options.edges {
let etype_id = db.get_or_create_etype(&edge_def.name);
edge_def.etype_id = Some(etype_id);
for prop_name in edge_def.props.keys() {
let prop_key_id = db.get_or_create_propkey(prop_name);
edge_def.prop_key_ids.insert(prop_name.clone(), prop_key_id);
}
edges.insert(edge_def.name.clone(), edge_def);
}
Ok(Self {
db,
nodes,
edges,
key_prefix_to_node,
})
}
pub fn create_node(
&mut self,
node_type: &str,
key_suffix: &str,
props: HashMap<String, PropValue>,
) -> Result<NodeRef> {
let node_def = self
.nodes
.get(node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?
.clone();
let full_key = node_def.key(key_suffix);
let mut handle = begin_tx(&self.db)?;
let node_opts = NodeOpts {
key: Some(full_key.clone()),
labels: node_def.label_id.map(|id| vec![id]),
props: None,
};
let node_id = create_node(&mut handle, node_opts)?;
for (prop_name, value) in props {
if let Some(&prop_key_id) = node_def.prop_key_ids.get(&prop_name) {
set_node_prop(&mut handle, node_id, prop_key_id, value)?;
}
}
commit(&mut handle)?;
Ok(NodeRef::new(node_id, Some(full_key), node_type))
}
pub fn insert(&mut self, node_type: &str) -> Result<RayInsertBuilder<'_>> {
let key_prefix = self
.nodes
.get(node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?
.key_prefix
.clone();
Ok(RayInsertBuilder {
ray: self,
node_type: node_type.to_string(),
key_prefix,
})
}
pub fn get(&self, node_type: &str, key_suffix: &str) -> Result<Option<NodeRef>> {
let node_def = self
.nodes
.get(node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?;
let full_key = node_def.key(key_suffix);
let node_id = get_node_by_key_db(&self.db, &full_key);
match node_id {
Some(id) => Ok(Some(NodeRef::new(id, Some(full_key), node_type))),
None => Ok(None),
}
}
pub fn get_by_id(&self, node_id: NodeId) -> Result<Option<NodeRef>> {
let exists = node_exists_db(&self.db, node_id);
if exists {
let delta = self.db.delta.read();
let key = get_node_key(self.db.snapshot.as_ref(), &delta, node_id);
let node_type = if let Some(ref k) = key {
self
.nodes
.values()
.find(|def| k.starts_with(&def.key_prefix))
.map(|def| def.name.as_str())
.unwrap_or("unknown")
} else {
"unknown"
};
Ok(Some(NodeRef::new(node_id, key, node_type)))
} else {
Ok(None)
}
}
pub fn exists(&self, node_id: NodeId) -> bool {
node_exists_db(&self.db, node_id)
}
pub fn delete_node(&mut self, node_id: NodeId) -> Result<bool> {
let mut handle = begin_tx(&self.db)?;
let deleted = delete_node(&mut handle, node_id)?;
commit(&mut handle)?;
Ok(deleted)
}
pub fn get_prop(&self, node_id: NodeId, prop_name: &str) -> Option<PropValue> {
let prop_key_id = self.db.get_propkey_id(prop_name)?;
get_node_prop_db(&self.db, node_id, prop_key_id)
}
pub fn set_prop(&mut self, node_id: NodeId, prop_name: &str, value: PropValue) -> Result<()> {
let prop_key_id = self.db.get_or_create_propkey(prop_name);
let mut handle = begin_tx(&self.db)?;
set_node_prop(&mut handle, node_id, prop_key_id, value)?;
commit(&mut handle)?;
Ok(())
}
pub fn update(&mut self, node_ref: &NodeRef) -> Result<RayUpdateNodeBuilder<'_>> {
let mut handle = begin_tx(&self.db)?;
let exists = node_exists(&handle, node_ref.id);
commit(&mut handle)?;
if !exists {
return Err(RayError::NodeNotFound(node_ref.id));
}
Ok(RayUpdateNodeBuilder {
ray: self,
node_id: node_ref.id,
updates: HashMap::new(),
})
}
pub fn update_by_id(&mut self, node_id: NodeId) -> Result<RayUpdateNodeBuilder<'_>> {
let mut handle = begin_tx(&self.db)?;
let exists = node_exists(&handle, node_id);
commit(&mut handle)?;
if !exists {
return Err(RayError::NodeNotFound(node_id));
}
Ok(RayUpdateNodeBuilder {
ray: self,
node_id,
updates: HashMap::new(),
})
}
pub fn update_by_key(
&mut self,
node_type: &str,
key_suffix: &str,
) -> Result<RayUpdateNodeBuilder<'_>> {
let full_key = self
.nodes
.get(node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?
.key(key_suffix);
let mut handle = begin_tx(&self.db)?;
let node_id =
get_node_by_key(&handle, &full_key).ok_or_else(|| RayError::KeyNotFound(full_key.clone()))?;
commit(&mut handle)?;
Ok(RayUpdateNodeBuilder {
ray: self,
node_id,
updates: HashMap::new(),
})
}
pub fn link(&mut self, src: NodeId, edge_type: &str, dst: NodeId) -> Result<()> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
let mut handle = begin_tx(&self.db)?;
add_edge(&mut handle, src, etype_id, dst)?;
commit(&mut handle)?;
Ok(())
}
pub fn link_with_props(
&mut self,
src: NodeId,
edge_type: &str,
dst: NodeId,
props: HashMap<String, PropValue>,
) -> Result<()> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?
.clone();
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
let mut handle = begin_tx(&self.db)?;
add_edge(&mut handle, src, etype_id, dst)?;
for (prop_name, value) in props {
let prop_key_id = if let Some(&id) = edge_def.prop_key_ids.get(&prop_name) {
id
} else {
handle.db.get_or_create_propkey(&prop_name)
};
set_edge_prop(&mut handle, src, etype_id, dst, prop_key_id, value)?;
}
commit(&mut handle)?;
Ok(())
}
pub fn unlink(&mut self, src: NodeId, edge_type: &str, dst: NodeId) -> Result<bool> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
let mut handle = begin_tx(&self.db)?;
let deleted = delete_edge(&mut handle, src, etype_id, dst)?;
commit(&mut handle)?;
Ok(deleted)
}
pub fn has_edge(&self, src: NodeId, edge_type: &str, dst: NodeId) -> Result<bool> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
Ok(edge_exists_db(&self.db, src, etype_id, dst))
}
pub fn neighbors_out(&self, node_id: NodeId, edge_type: Option<&str>) -> Result<Vec<NodeId>> {
let etype_id = match edge_type {
Some(name) => {
let edge_def = self
.edges
.get(name)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {name}")))?;
edge_def.etype_id
}
None => None,
};
Ok(get_neighbors_out_db(&self.db, node_id, etype_id))
}
pub fn neighbors_in(&self, node_id: NodeId, edge_type: Option<&str>) -> Result<Vec<NodeId>> {
let etype_id = match edge_type {
Some(name) => {
let edge_def = self
.edges
.get(name)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {name}")))?;
edge_def.etype_id
}
None => None,
};
let neighbors = get_neighbors_in_db(&self.db, node_id, etype_id);
Ok(neighbors)
}
pub fn get_edge_prop(
&self,
src: NodeId,
edge_type: &str,
dst: NodeId,
prop_name: &str,
) -> Result<Option<PropValue>> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
let prop_key_id = match self.db.get_propkey_id(prop_name) {
Some(id) => id,
None => return Ok(None), };
Ok(get_edge_prop_db(&self.db, src, etype_id, dst, prop_key_id))
}
pub fn get_edge_props(
&self,
src: NodeId,
edge_type: &str,
dst: NodeId,
) -> Result<Option<HashMap<String, PropValue>>> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
let props = get_edge_props_db(&self.db, src, etype_id, dst);
match props {
Some(props_by_id) => {
let mut result = HashMap::new();
for (key_id, value) in props_by_id {
if let Some(name) = self.db.get_propkey_name(key_id) {
result.insert(name, value);
}
}
Ok(Some(result))
}
None => Ok(None),
}
}
pub fn set_edge_prop(
&mut self,
src: NodeId,
edge_type: &str,
dst: NodeId,
prop_name: &str,
value: PropValue,
) -> Result<()> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
let prop_key_id = self.db.get_or_create_propkey(prop_name);
let mut handle = begin_tx(&self.db)?;
set_edge_prop(&mut handle, src, etype_id, dst, prop_key_id, value)?;
commit(&mut handle)?;
Ok(())
}
pub fn del_edge_prop(
&mut self,
src: NodeId,
edge_type: &str,
dst: NodeId,
prop_name: &str,
) -> Result<()> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
let prop_key_id = self
.db
.get_propkey_id(prop_name)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown property: {prop_name}")))?;
let mut handle = begin_tx(&self.db)?;
del_edge_prop(&mut handle, src, etype_id, dst, prop_key_id)?;
commit(&mut handle)?;
Ok(())
}
pub fn update_edge(
&mut self,
src: NodeId,
edge_type: &str,
dst: NodeId,
) -> Result<RayUpdateEdgeBuilder<'_>> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
Ok(RayUpdateEdgeBuilder {
ray: self,
src,
etype_id,
dst,
updates: HashMap::new(),
})
}
pub fn count_nodes(&self) -> u64 {
count_nodes(&self.db)
}
pub fn count_nodes_by_type(&self, node_type: &str) -> Result<u64> {
let node_def = self
.nodes
.get(node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?;
let prefix = &node_def.key_prefix;
let mut count = 0u64;
for node_id in list_nodes(&self.db) {
if let Some(key) = self.get_node_key_internal(node_id) {
if key.starts_with(prefix) {
count += 1;
}
}
}
Ok(count)
}
pub fn count_edges(&self) -> u64 {
count_edges(&self.db, None)
}
pub fn count_edges_by_type(&self, edge_type: &str) -> Result<u64> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
Ok(count_edges(&self.db, Some(etype_id)))
}
pub fn list_nodes(&self) -> Vec<NodeId> {
list_nodes(&self.db)
}
pub fn all(&self, node_type: &str) -> Result<impl Iterator<Item = NodeRef> + '_> {
let node_def = self
.nodes
.get(node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?
.clone();
let prefix = node_def.key_prefix.clone();
let node_type_str = node_type.to_string();
Ok(list_nodes(&self.db).into_iter().filter_map(move |node_id| {
let key = self.get_node_key_internal(node_id)?;
if key.starts_with(&prefix) {
Some(NodeRef::new(node_id, Some(key), &node_type_str))
} else {
None
}
}))
}
pub fn list_all_edges(&self) -> Vec<FullEdge> {
list_edges(&self.db, ListEdgesOptions::default())
}
pub fn all_edges(&self, edge_type: Option<&str>) -> Result<impl Iterator<Item = FullEdge> + '_> {
let etype_id = match edge_type {
Some(name) => {
let edge_def = self
.edges
.get(name)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {name}")))?;
edge_def.etype_id
}
None => None,
};
let options = ListEdgesOptions { etype: etype_id };
Ok(list_edges(&self.db, options).into_iter())
}
pub fn get_ref(&self, node_type: &str, key_suffix: &str) -> Result<Option<NodeRef>> {
let node_def = self
.nodes
.get(node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?;
let full_key = node_def.key(key_suffix);
let node_id = get_node_by_key_db(&self.db, &full_key);
match node_id {
Some(id) => Ok(Some(NodeRef::new(id, Some(full_key), node_type))),
None => Ok(None),
}
}
fn get_node_key_internal(&self, node_id: NodeId) -> Option<String> {
let delta = self.db.delta.read();
get_node_key(self.db.snapshot.as_ref(), &delta, node_id)
}
pub fn node_def(&self, name: &str) -> Option<&NodeDef> {
self.nodes.get(name)
}
pub fn edge_def(&self, name: &str) -> Option<&EdgeDef> {
self.edges.get(name)
}
pub fn node_types(&self) -> Vec<&str> {
self.nodes.keys().map(|s| s.as_str()).collect()
}
pub fn edge_types(&self) -> Vec<&str> {
self.edges.keys().map(|s| s.as_str()).collect()
}
pub fn from(&self, node_id: NodeId) -> RayTraversalBuilder<'_> {
RayTraversalBuilder::new(self, vec![node_id])
}
pub fn from_nodes(&self, node_ids: Vec<NodeId>) -> RayTraversalBuilder<'_> {
RayTraversalBuilder::new(self, node_ids)
}
pub fn shortest_path(&self, source: NodeId, target: NodeId) -> RayPathBuilder<'_> {
RayPathBuilder::new(self, source, target)
}
pub fn shortest_path_to_any(&self, source: NodeId, targets: Vec<NodeId>) -> RayPathBuilder<'_> {
RayPathBuilder::new_multi(self, source, targets)
}
pub fn has_path(
&mut self,
source: NodeId,
target: NodeId,
edge_type: Option<&str>,
) -> Result<bool> {
let path = self.shortest_path(source, target);
let path = if let Some(etype) = edge_type {
path.via(etype)?
} else {
path
};
Ok(path.find().found)
}
pub fn reachable_from(
&self,
source: NodeId,
max_depth: usize,
edge_type: Option<&str>,
) -> Result<Vec<NodeId>> {
let etype = match edge_type {
Some(name) => {
let edge_def = self
.edges
.get(name)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {name}")))?;
edge_def.etype_id
}
None => None,
};
use super::traversal::{TraversalBuilder, TraversalDirection, TraverseOptions};
let options = TraverseOptions::new(TraversalDirection::Out, max_depth);
let results = TraversalBuilder::from_node(source)
.traverse(etype, options)
.collect_node_ids(|node_id, dir, etype_filter| {
self.get_neighbors(node_id, dir, etype_filter)
});
Ok(results)
}
fn get_neighbors(
&self,
node_id: NodeId,
direction: super::traversal::TraversalDirection,
etype: Option<ETypeId>,
) -> Vec<Edge> {
use super::traversal::TraversalDirection;
let mut edges = Vec::new();
let delta = self.db.delta.read();
match direction {
TraversalDirection::Out => {
let deleted_set = delta.out_del.get(&node_id);
if let Some(ref snapshot) = self.db.snapshot {
if let Some(src_phys) = snapshot.get_phys_node(node_id) {
for (dst_phys, edge_etype) in snapshot.iter_out_edges(src_phys) {
if etype.is_some() && etype != Some(edge_etype) {
continue;
}
if let Some(dst_id) = snapshot.get_node_id(dst_phys) {
let is_deleted = deleted_set
.map(|set| {
set.contains(&EdgePatch {
etype: edge_etype,
other: dst_id,
})
})
.unwrap_or(false);
if !is_deleted {
edges.push(Edge {
src: node_id,
etype: edge_etype,
dst: dst_id,
});
}
}
}
}
}
if let Some(add_set) = delta.out_add.get(&node_id) {
for patch in add_set {
if etype.is_none() || etype == Some(patch.etype) {
if !edges
.iter()
.any(|e| e.dst == patch.other && e.etype == patch.etype)
{
edges.push(Edge {
src: node_id,
etype: patch.etype,
dst: patch.other,
});
}
}
}
}
}
TraversalDirection::In => {
let deleted_set = delta.in_del.get(&node_id);
if let Some(ref snapshot) = self.db.snapshot {
if let Some(dst_phys) = snapshot.get_phys_node(node_id) {
for (src_phys, edge_etype, _out_index) in snapshot.iter_in_edges(dst_phys) {
if etype.is_some() && etype != Some(edge_etype) {
continue;
}
if let Some(src_id) = snapshot.get_node_id(src_phys) {
let is_deleted = deleted_set
.map(|set| {
set.contains(&EdgePatch {
etype: edge_etype,
other: src_id,
})
})
.unwrap_or(false);
if !is_deleted {
edges.push(Edge {
src: src_id,
etype: edge_etype,
dst: node_id,
});
}
}
}
}
}
if let Some(add_set) = delta.in_add.get(&node_id) {
for patch in add_set {
if etype.is_none() || etype == Some(patch.etype) {
if !edges
.iter()
.any(|e| e.src == patch.other && e.etype == patch.etype)
{
edges.push(Edge {
src: patch.other,
etype: patch.etype,
dst: node_id,
});
}
}
}
}
}
TraversalDirection::Both => {
drop(delta); edges.extend(self.get_neighbors(node_id, TraversalDirection::Out, etype));
edges.extend(self.get_neighbors(node_id, TraversalDirection::In, etype));
}
}
edges
}
pub fn optimize(&mut self) -> Result<()> {
self.db.optimize()
}
pub fn stats(&self) -> DbStats {
use crate::graph::iterators::{count_edges, count_nodes};
let node_count = count_nodes(&self.db);
let edge_count = count_edges(&self.db, None);
let delta = self.db.delta.read();
let delta_nodes_created = delta.created_nodes.len();
let delta_nodes_deleted = delta.deleted_nodes.len();
let delta_edges_added = delta.total_edges_added();
let delta_edges_deleted = delta.total_edges_deleted();
drop(delta);
let (snapshot_gen, snapshot_nodes, snapshot_edges, snapshot_max_node_id) =
if let Some(ref snapshot) = self.db.snapshot {
(
snapshot.header.generation,
snapshot.header.num_nodes,
snapshot.header.num_edges,
snapshot.header.max_node_id,
)
} else {
(0, 0, 0, 0)
};
let wal_segment = self
.db
.manifest
.as_ref()
.map(|m| m.active_wal_seg)
.unwrap_or(0);
let mvcc_stats = self.db.mvcc.as_ref().map(|mvcc| {
let tx_mgr = mvcc.tx_manager.lock();
let gc = mvcc.gc.lock();
let gc_stats = gc.get_stats();
let committed_stats = tx_mgr.get_committed_writes_stats();
MvccStats {
active_transactions: tx_mgr.get_active_count(),
min_active_ts: tx_mgr.min_active_ts(),
versions_pruned: gc_stats.versions_pruned,
gc_runs: gc_stats.gc_runs,
last_gc_time: gc_stats.last_gc_time,
committed_writes_size: committed_stats.size,
committed_writes_pruned: committed_stats.pruned,
}
});
let total_changes =
delta_nodes_created + delta_nodes_deleted + delta_edges_added + delta_edges_deleted;
let recommend_compact = total_changes > 10_000;
DbStats {
snapshot_gen,
snapshot_nodes: snapshot_nodes.max(node_count), snapshot_edges: snapshot_edges.max(edge_count),
snapshot_max_node_id,
delta_nodes_created,
delta_nodes_deleted,
delta_edges_added,
delta_edges_deleted,
wal_segment,
wal_bytes: self.db.wal_bytes(),
recommend_compact,
mvcc_stats,
}
}
pub fn describe(&self) -> String {
let stats = self.stats();
let path = self.db.path.display();
let format = if self.db.is_single_file {
"single-file"
} else {
"multi-file"
};
let node_types: Vec<&str> = self.nodes.keys().map(|s| s.as_str()).collect();
let edge_types: Vec<&str> = self.edges.keys().map(|s| s.as_str()).collect();
let delta_nodes = stats.delta_nodes_created as i64 - stats.delta_nodes_deleted as i64;
let delta_edges = stats.delta_edges_added as i64 - stats.delta_edges_deleted as i64;
format!(
"KiteDB at {} ({} format)\n\
Schema:\n \
Node types: {}\n \
Edge types: {}\n\
Statistics:\n \
Nodes: {} (snapshot: {}, delta: {:+})\n \
Edges: {} (snapshot: {}, delta: {:+})\n \
Recommend compact: {}",
path,
format,
if node_types.is_empty() {
"(none)".to_string()
} else {
node_types.join(", ")
},
if edge_types.is_empty() {
"(none)".to_string()
} else {
edge_types.join(", ")
},
stats.snapshot_nodes,
stats
.snapshot_nodes
.saturating_sub(stats.delta_nodes_created as u64),
delta_nodes,
stats.snapshot_edges,
stats
.snapshot_edges
.saturating_sub(stats.delta_edges_added as u64),
delta_edges,
if stats.recommend_compact { "yes" } else { "no" }
)
}
pub fn check(&self) -> Result<CheckResult> {
let mut result = if let Some(ref snapshot) = self.db.snapshot {
crate::check::check_snapshot(snapshot)
} else {
CheckResult {
valid: true,
errors: Vec::new(),
warnings: vec!["No snapshot to check".to_string()],
}
};
for (edge_name, edge_def) in &self.edges {
if edge_def.etype_id.is_none() {
result
.warnings
.push(format!("Edge type '{edge_name}' has no assigned etype_id"));
}
}
Ok(result)
}
pub fn raw(&self) -> &GraphDB {
&self.db
}
pub fn raw_mut(&mut self) -> &mut GraphDB {
&mut self.db
}
pub fn close(self) -> Result<()> {
close_graph_db(self.db)
}
}
use super::traversal::{TraversalBuilder, TraversalDirection, TraversalResult, TraverseOptions};
pub struct RayTraversalBuilder<'a> {
ray: &'a Ray,
builder: TraversalBuilder,
}
impl<'a> RayTraversalBuilder<'a> {
fn new(ray: &'a Ray, start_nodes: Vec<NodeId>) -> Self {
Self {
ray,
builder: TraversalBuilder::new(start_nodes),
}
}
pub fn out(mut self, edge_type: Option<&str>) -> Result<Self> {
let etype = self.resolve_etype(edge_type)?;
self.builder = self.builder.out(etype);
Ok(self)
}
pub fn r#in(mut self, edge_type: Option<&str>) -> Result<Self> {
let etype = self.resolve_etype(edge_type)?;
self.builder = self.builder.r#in(etype);
Ok(self)
}
pub fn both(mut self, edge_type: Option<&str>) -> Result<Self> {
let etype = self.resolve_etype(edge_type)?;
self.builder = self.builder.both(etype);
Ok(self)
}
pub fn traverse(mut self, edge_type: Option<&str>, options: TraverseOptions) -> Result<Self> {
let etype = self.resolve_etype(edge_type)?;
self.builder = self.builder.traverse(etype, options);
Ok(self)
}
pub fn take(mut self, limit: usize) -> Self {
self.builder = self.builder.take(limit);
self
}
pub fn select(mut self, props: &[&str]) -> Self {
self.builder = self.builder.select_props(props);
self
}
pub fn to_vec(self) -> Vec<NodeId> {
self
.builder
.collect_node_ids(|node_id, dir, etype| self.ray.get_neighbors(node_id, dir, etype))
}
pub fn first(self) -> Option<TraversalResult> {
self
.builder
.first(|node_id, dir, etype| self.ray.get_neighbors(node_id, dir, etype))
}
pub fn first_node(self) -> Option<NodeId> {
self
.builder
.first_node(|node_id, dir, etype| self.ray.get_neighbors(node_id, dir, etype))
}
pub fn count(self) -> usize {
self
.builder
.count(|node_id, dir, etype| self.ray.get_neighbors(node_id, dir, etype))
}
pub fn execute(self) -> impl Iterator<Item = TraversalResult> + 'a {
let ray = self.ray;
self
.builder
.execute(move |node_id, dir, etype| ray.get_neighbors(node_id, dir, etype))
}
pub fn edges(self) -> impl Iterator<Item = Edge> + 'a {
let ray = self.ray;
self
.builder
.execute(move |node_id, dir, etype| ray.get_neighbors(node_id, dir, etype))
.filter_map(|result| {
result.edge.map(|e| Edge {
src: e.src,
etype: e.etype,
dst: e.dst,
})
})
}
pub fn full_edges(self) -> impl Iterator<Item = FullEdge> + 'a {
let ray = self.ray;
self
.builder
.execute(move |node_id, dir, etype| ray.get_neighbors(node_id, dir, etype))
.filter_map(move |result| {
result.edge.map(|e| FullEdge {
src: e.src,
etype: e.etype,
dst: e.dst,
})
})
}
fn resolve_etype(&self, edge_type: Option<&str>) -> Result<Option<ETypeId>> {
match edge_type {
Some(name) => {
let edge_def = self
.ray
.edges
.get(name)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {name}")))?;
Ok(edge_def.etype_id)
}
None => Ok(None),
}
}
}
use super::pathfinding::{bfs, dijkstra, yen_k_shortest, PathConfig, PathResult};
pub struct RayPathBuilder<'a> {
ray: &'a Ray,
source: NodeId,
targets: HashSet<NodeId>,
allowed_etypes: HashSet<ETypeId>,
direction: TraversalDirection,
max_depth: usize,
weights: HashMap<(NodeId, ETypeId, NodeId), f64>,
}
impl<'a> RayPathBuilder<'a> {
fn new(ray: &'a Ray, source: NodeId, target: NodeId) -> Self {
let mut targets = HashSet::new();
targets.insert(target);
Self {
ray,
source,
targets,
allowed_etypes: HashSet::new(),
direction: TraversalDirection::Out,
max_depth: 100,
weights: HashMap::new(),
}
}
fn new_multi(ray: &'a Ray, source: NodeId, targets: Vec<NodeId>) -> Self {
Self {
ray,
source,
targets: targets.into_iter().collect(),
allowed_etypes: HashSet::new(),
direction: TraversalDirection::Out,
max_depth: 100,
weights: HashMap::new(),
}
}
pub fn via(mut self, edge_type: &str) -> Result<Self> {
let edge_def = self
.ray
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
if let Some(etype_id) = edge_def.etype_id {
self.allowed_etypes.insert(etype_id);
}
Ok(self)
}
pub fn max_depth(mut self, depth: usize) -> Self {
self.max_depth = depth;
self
}
pub fn direction(mut self, direction: TraversalDirection) -> Self {
self.direction = direction;
self
}
pub fn bidirectional(mut self) -> Self {
self.direction = TraversalDirection::Both;
self
}
pub fn find(self) -> PathResult {
let config = PathConfig {
source: self.source,
targets: self.targets,
allowed_etypes: self.allowed_etypes,
direction: self.direction,
max_depth: self.max_depth,
};
let weights = self.weights;
dijkstra(
config,
|node_id, dir, etype| self.ray.get_neighbors(node_id, dir, etype),
move |src, etype, dst| weights.get(&(src, etype, dst)).copied().unwrap_or(1.0),
)
}
pub fn find_bfs(self) -> PathResult {
let config = PathConfig {
source: self.source,
targets: self.targets,
allowed_etypes: self.allowed_etypes,
direction: self.direction,
max_depth: self.max_depth,
};
bfs(config, |node_id, dir, etype| {
self.ray.get_neighbors(node_id, dir, etype)
})
}
pub fn find_k_shortest(self, k: usize) -> Vec<PathResult> {
let config = PathConfig {
source: self.source,
targets: self.targets,
allowed_etypes: self.allowed_etypes,
direction: self.direction,
max_depth: self.max_depth,
};
let weights = self.weights;
yen_k_shortest(
config,
k,
|node_id, dir, etype| self.ray.get_neighbors(node_id, dir, etype),
move |src, etype, dst| weights.get(&(src, etype, dst)).copied().unwrap_or(1.0),
)
}
}
#[derive(Debug, Clone)]
pub enum BatchOp {
CreateNode {
node_type: String,
key_suffix: String,
props: HashMap<String, PropValue>,
},
DeleteNode { node_id: NodeId },
Link {
src: NodeId,
edge_type: String,
dst: NodeId,
},
Unlink {
src: NodeId,
edge_type: String,
dst: NodeId,
},
SetProp {
node_id: NodeId,
prop_name: String,
value: PropValue,
},
DelProp { node_id: NodeId, prop_name: String },
}
#[derive(Debug, Clone)]
pub enum BatchResult {
NodeCreated(NodeRef),
NodeDeleted(bool),
EdgeCreated,
EdgeRemoved(bool),
PropSet,
PropDeleted,
}
impl Ray {
pub fn batch(&mut self, ops: Vec<BatchOp>) -> Result<Vec<BatchResult>> {
let mut handle = begin_tx(&self.db)?;
let mut results = Vec::with_capacity(ops.len());
for op in ops {
let result = match op {
BatchOp::CreateNode {
node_type,
key_suffix,
props,
} => {
let node_def = self
.nodes
.get(&node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?;
let full_key = node_def.key(&key_suffix);
let node_opts = NodeOpts {
key: Some(full_key.clone()),
labels: node_def.label_id.map(|id| vec![id]),
props: None,
};
let node_id = create_node(&mut handle, node_opts)?;
for (prop_name, value) in props {
if let Some(&prop_key_id) = node_def.prop_key_ids.get(&prop_name) {
set_node_prop(&mut handle, node_id, prop_key_id, value)?;
}
}
BatchResult::NodeCreated(NodeRef::new(node_id, Some(full_key), &node_type))
}
BatchOp::DeleteNode { node_id } => {
let deleted = delete_node(&mut handle, node_id)?;
BatchResult::NodeDeleted(deleted)
}
BatchOp::Link {
src,
edge_type,
dst,
} => {
let edge_def = self
.edges
.get(&edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
add_edge(&mut handle, src, etype_id, dst)?;
BatchResult::EdgeCreated
}
BatchOp::Unlink {
src,
edge_type,
dst,
} => {
let edge_def = self
.edges
.get(&edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
let deleted = delete_edge(&mut handle, src, etype_id, dst)?;
BatchResult::EdgeRemoved(deleted)
}
BatchOp::SetProp {
node_id,
prop_name,
value,
} => {
let prop_key_id = handle.db.get_or_create_propkey(&prop_name);
set_node_prop(&mut handle, node_id, prop_key_id, value)?;
BatchResult::PropSet
}
BatchOp::DelProp { node_id, prop_name } => {
let prop_key_id = handle
.db
.get_propkey_id(&prop_name)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown property: {prop_name}")))?;
del_node_prop(&mut handle, node_id, prop_key_id)?;
BatchResult::PropDeleted
}
};
results.push(result);
}
commit(&mut handle)?;
Ok(results)
}
}
pub struct TxContext<'a> {
handle: TxHandle<'a>,
nodes: &'a HashMap<String, NodeDef>,
edges: &'a HashMap<String, EdgeDef>,
}
impl<'a> TxContext<'a> {
pub fn create_node(
&mut self,
node_type: &str,
key_suffix: &str,
props: HashMap<String, PropValue>,
) -> Result<NodeRef> {
let node_def = self
.nodes
.get(node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?
.clone();
let full_key = node_def.key(key_suffix);
let node_opts = NodeOpts {
key: Some(full_key.clone()),
labels: node_def.label_id.map(|id| vec![id]),
props: None,
};
let node_id = create_node(&mut self.handle, node_opts)?;
for (prop_name, value) in props {
if let Some(&prop_key_id) = node_def.prop_key_ids.get(&prop_name) {
set_node_prop(&mut self.handle, node_id, prop_key_id, value)?;
}
}
Ok(NodeRef::new(node_id, Some(full_key), node_type))
}
pub fn delete_node(&mut self, node_id: NodeId) -> Result<bool> {
delete_node(&mut self.handle, node_id)
}
pub fn link(&mut self, src: NodeId, edge_type: &str, dst: NodeId) -> Result<()> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
add_edge(&mut self.handle, src, etype_id, dst)?;
Ok(())
}
pub fn unlink(&mut self, src: NodeId, edge_type: &str, dst: NodeId) -> Result<bool> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
delete_edge(&mut self.handle, src, etype_id, dst)
}
pub fn set_prop(&mut self, node_id: NodeId, prop_name: &str, value: PropValue) -> Result<()> {
let prop_key_id = self.handle.db.get_or_create_propkey(prop_name);
set_node_prop(&mut self.handle, node_id, prop_key_id, value)?;
Ok(())
}
pub fn del_prop(&mut self, node_id: NodeId, prop_name: &str) -> Result<()> {
let prop_key_id = self
.handle
.db
.get_propkey_id(prop_name)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown property: {prop_name}")))?;
del_node_prop(&mut self.handle, node_id, prop_key_id)?;
Ok(())
}
pub fn exists(&self, node_id: NodeId) -> bool {
node_exists(&self.handle, node_id)
}
pub fn has_edge(&self, src: NodeId, edge_type: &str, dst: NodeId) -> Result<bool> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown edge type: {edge_type}")))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| RayError::InvalidSchema("Edge type not initialized".to_string()))?;
Ok(edge_exists(&self.handle, src, etype_id, dst))
}
pub fn get_prop(&self, node_id: NodeId, prop_name: &str) -> Result<Option<PropValue>> {
let prop_key_id = self
.handle
.db
.get_propkey_id(prop_name)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown property: {prop_name}")))?;
Ok(get_node_prop(&self.handle, node_id, prop_key_id))
}
pub fn get(&self, node_type: &str, key_suffix: &str) -> Result<Option<NodeRef>> {
let node_def = self
.nodes
.get(node_type)
.ok_or_else(|| RayError::InvalidSchema(format!("Unknown node type: {node_type}")))?;
let full_key = node_def.key(key_suffix);
let node_id = get_node_by_key(&self.handle, &full_key);
match node_id {
Some(id) => Ok(Some(NodeRef::new(id, Some(full_key), node_type))),
None => Ok(None),
}
}
}
impl Ray {
pub fn transaction<T, F>(&mut self, f: F) -> Result<T>
where
F: FnOnce(&mut TxContext) -> Result<T>,
{
let handle = begin_tx(&self.db)?;
let mut ctx = TxContext {
handle,
nodes: &self.nodes,
edges: &self.edges,
};
match f(&mut ctx) {
Ok(result) => {
commit(&mut ctx.handle)?;
Ok(result)
}
Err(e) => {
rollback(&mut ctx.handle)?;
Err(e)
}
}
}
pub fn tx(&mut self) -> TxBuilder {
TxBuilder { ops: Vec::new() }
}
}
#[derive(Debug, Default)]
pub struct TxBuilder {
ops: Vec<BatchOp>,
}
impl TxBuilder {
pub fn create_node(
mut self,
node_type: impl Into<String>,
key_suffix: impl Into<String>,
props: HashMap<String, PropValue>,
) -> Self {
self.ops.push(BatchOp::CreateNode {
node_type: node_type.into(),
key_suffix: key_suffix.into(),
props,
});
self
}
pub fn delete_node(mut self, node_id: NodeId) -> Self {
self.ops.push(BatchOp::DeleteNode { node_id });
self
}
pub fn link(mut self, src: NodeId, edge_type: impl Into<String>, dst: NodeId) -> Self {
self.ops.push(BatchOp::Link {
src,
edge_type: edge_type.into(),
dst,
});
self
}
pub fn unlink(mut self, src: NodeId, edge_type: impl Into<String>, dst: NodeId) -> Self {
self.ops.push(BatchOp::Unlink {
src,
edge_type: edge_type.into(),
dst,
});
self
}
pub fn set_prop(
mut self,
node_id: NodeId,
prop_name: impl Into<String>,
value: PropValue,
) -> Self {
self.ops.push(BatchOp::SetProp {
node_id,
prop_name: prop_name.into(),
value,
});
self
}
pub fn del_prop(mut self, node_id: NodeId, prop_name: impl Into<String>) -> Self {
self.ops.push(BatchOp::DelProp {
node_id,
prop_name: prop_name.into(),
});
self
}
pub fn execute(self, ray: &mut Ray) -> Result<Vec<BatchResult>> {
ray.batch(self.ops)
}
pub fn into_ops(self) -> Vec<BatchOp> {
self.ops
}
}
pub struct RayUpdateNodeBuilder<'a> {
ray: &'a mut Ray,
node_id: NodeId,
updates: HashMap<String, Option<PropValue>>,
}
impl<'a> RayUpdateNodeBuilder<'a> {
pub fn set(mut self, prop_name: &str, value: PropValue) -> Self {
self.updates.insert(prop_name.to_string(), Some(value));
self
}
pub fn unset(mut self, prop_name: &str) -> Self {
self.updates.insert(prop_name.to_string(), None);
self
}
pub fn set_all(mut self, props: HashMap<String, PropValue>) -> Self {
for (k, v) in props {
self.updates.insert(k, Some(v));
}
self
}
pub fn execute(self) -> Result<()> {
if self.updates.is_empty() {
return Ok(());
}
let mut handle = begin_tx(&self.ray.db)?;
for (prop_name, value_opt) in self.updates {
let prop_key_id = self.ray.db.get_or_create_propkey(&prop_name);
match value_opt {
Some(value) => {
set_node_prop(&mut handle, self.node_id, prop_key_id, value)?;
}
None => {
del_node_prop(&mut handle, self.node_id, prop_key_id)?;
}
}
}
commit(&mut handle)?;
Ok(())
}
pub fn node_id(&self) -> NodeId {
self.node_id
}
}
pub struct RayInsertBuilder<'a> {
ray: &'a mut Ray,
node_type: String,
key_prefix: String,
}
impl<'a> RayInsertBuilder<'a> {
pub fn values(
self,
key_suffix: &str,
props: HashMap<String, PropValue>,
) -> Result<InsertExecutorSingle<'a>> {
let full_key = format!("{}{}", self.key_prefix, key_suffix);
Ok(InsertExecutorSingle {
ray: self.ray,
node_type: self.node_type,
full_key,
props,
})
}
pub fn values_many(
self,
items: Vec<(&str, HashMap<String, PropValue>)>,
) -> Result<InsertExecutorMultiple<'a>> {
let entries: Vec<(String, HashMap<String, PropValue>)> = items
.into_iter()
.map(|(key_suffix, props)| {
let full_key = format!("{}{}", self.key_prefix, key_suffix);
(full_key, props)
})
.collect();
Ok(InsertExecutorMultiple {
ray: self.ray,
node_type: self.node_type,
entries,
})
}
}
pub struct InsertExecutorSingle<'a> {
ray: &'a mut Ray,
node_type: String,
full_key: String,
props: HashMap<String, PropValue>,
}
impl<'a> InsertExecutorSingle<'a> {
pub fn returning(self) -> Result<NodeRef> {
let mut handle = begin_tx(&self.ray.db)?;
let node_opts = NodeOpts::new().with_key(self.full_key.clone());
let node_id = create_node(&mut handle, node_opts)?;
for (prop_name, value) in self.props {
let prop_key_id = self.ray.db.get_or_create_propkey(&prop_name);
set_node_prop(&mut handle, node_id, prop_key_id, value)?;
}
commit(&mut handle)?;
Ok(NodeRef::new(node_id, Some(self.full_key), &self.node_type))
}
pub fn execute(self) -> Result<()> {
let _ = self.returning()?;
Ok(())
}
}
pub struct InsertExecutorMultiple<'a> {
ray: &'a mut Ray,
node_type: String,
entries: Vec<(String, HashMap<String, PropValue>)>,
}
impl<'a> InsertExecutorMultiple<'a> {
pub fn returning(self) -> Result<Vec<NodeRef>> {
if self.entries.is_empty() {
return Ok(Vec::new());
}
let mut handle = begin_tx(&self.ray.db)?;
let mut results = Vec::with_capacity(self.entries.len());
for (full_key, props) in self.entries {
let node_opts = NodeOpts::new().with_key(full_key.clone());
let node_id = create_node(&mut handle, node_opts)?;
for (prop_name, value) in props {
let prop_key_id = self.ray.db.get_or_create_propkey(&prop_name);
set_node_prop(&mut handle, node_id, prop_key_id, value)?;
}
results.push(NodeRef::new(node_id, Some(full_key), &self.node_type));
}
commit(&mut handle)?;
Ok(results)
}
pub fn execute(self) -> Result<()> {
let _ = self.returning()?;
Ok(())
}
}
pub struct RayUpdateEdgeBuilder<'a> {
ray: &'a mut Ray,
src: NodeId,
etype_id: ETypeId,
dst: NodeId,
updates: HashMap<String, Option<PropValue>>,
}
impl<'a> RayUpdateEdgeBuilder<'a> {
pub fn set(mut self, prop_name: &str, value: PropValue) -> Self {
self.updates.insert(prop_name.to_string(), Some(value));
self
}
pub fn unset(mut self, prop_name: &str) -> Self {
self.updates.insert(prop_name.to_string(), None);
self
}
pub fn set_all(mut self, props: HashMap<String, PropValue>) -> Self {
for (k, v) in props {
self.updates.insert(k, Some(v));
}
self
}
pub fn execute(self) -> Result<()> {
if self.updates.is_empty() {
return Ok(());
}
let mut handle = begin_tx(&self.ray.db)?;
for (prop_name, value_opt) in self.updates {
let prop_key_id = self.ray.db.get_or_create_propkey(&prop_name);
match value_opt {
Some(value) => {
set_edge_prop(
&mut handle,
self.src,
self.etype_id,
self.dst,
prop_key_id,
value,
)?;
}
None => {
if let Some(existing_key_id) = self.ray.db.get_propkey_id(&prop_name) {
del_edge_prop(
&mut handle,
self.src,
self.etype_id,
self.dst,
existing_key_id,
)?;
}
}
}
}
commit(&mut handle)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn create_test_schema() -> RayOptions {
let user = NodeDef::new("User", "user:")
.prop(PropDef::string("name").required())
.prop(PropDef::int("age"));
let post = NodeDef::new("Post", "post:")
.prop(PropDef::string("title").required())
.prop(PropDef::string("content"));
let follows = EdgeDef::new("FOLLOWS");
let authored = EdgeDef::new("AUTHORED");
RayOptions::new()
.node(user)
.node(post)
.edge(follows)
.edge(authored)
}
#[test]
fn test_open_database() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let ray = Ray::open(temp_dir.path(), options).unwrap();
assert_eq!(ray.node_types().len(), 2);
assert_eq!(ray.edge_types().len(), 2);
assert!(ray.node_def("User").is_some());
assert!(ray.edge_def("FOLLOWS").is_some());
ray.close().unwrap();
}
#[test]
fn test_create_and_get_node() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Alice".to_string()));
props.insert("age".to_string(), PropValue::I64(30));
let user_ref = ray.create_node("User", "alice", props).unwrap();
assert!(user_ref.id > 0);
assert_eq!(user_ref.key, Some("user:alice".to_string()));
let found = ray.get("User", "alice").unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().id, user_ref.id);
let not_found = ray.get("User", "bob").unwrap();
assert!(not_found.is_none());
ray.close().unwrap();
}
#[test]
fn test_link_and_unlink() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
assert!(ray.has_edge(alice.id, "FOLLOWS", bob.id).unwrap());
assert!(!ray.has_edge(bob.id, "FOLLOWS", alice.id).unwrap());
let alice_follows = ray.neighbors_out(alice.id, Some("FOLLOWS")).unwrap();
assert_eq!(alice_follows, vec![bob.id]);
let bob_followers = ray.neighbors_in(bob.id, Some("FOLLOWS")).unwrap();
assert_eq!(bob_followers, vec![alice.id]);
ray.unlink(alice.id, "FOLLOWS", bob.id).unwrap();
assert!(!ray.has_edge(alice.id, "FOLLOWS", bob.id).unwrap());
ray.close().unwrap();
}
#[test]
fn test_properties() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Alice".to_string()));
let user = ray.create_node("User", "alice", props).unwrap();
let name = ray.get_prop(user.id, "name");
assert_eq!(name, Some(PropValue::String("Alice".to_string())));
ray.set_prop(user.id, "age", PropValue::I64(25)).unwrap();
let age = ray.get_prop(user.id, "age");
assert_eq!(age, Some(PropValue::I64(25)));
ray.close().unwrap();
}
#[test]
fn test_count_nodes() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
assert_eq!(ray.count_nodes(), 0);
ray.create_node("User", "alice", HashMap::new()).unwrap();
ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.create_node("Post", "post1", HashMap::new()).unwrap();
assert_eq!(ray.count_nodes(), 3);
ray.close().unwrap();
}
#[test]
fn test_delete_node() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let user = ray.create_node("User", "alice", HashMap::new()).unwrap();
assert!(ray.exists(user.id));
ray.delete_node(user.id).unwrap();
assert!(!ray.exists(user.id));
ray.close().unwrap();
}
#[test]
fn test_get_ref() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let user = ray.create_node("User", "alice", HashMap::new()).unwrap();
let node_ref = ray.get_ref("User", "alice").unwrap();
assert!(node_ref.is_some());
let node_ref = node_ref.unwrap();
assert_eq!(node_ref.id, user.id);
assert_eq!(node_ref.key, Some("user:alice".to_string()));
let not_found = ray.get_ref("User", "bob").unwrap();
assert!(not_found.is_none());
ray.close().unwrap();
}
#[test]
fn test_all_nodes_by_type() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
ray.create_node("User", "alice", HashMap::new()).unwrap();
ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.create_node("Post", "post1", HashMap::new()).unwrap();
let users: Vec<_> = ray.all("User").unwrap().collect();
assert_eq!(users.len(), 2);
let posts: Vec<_> = ray.all("Post").unwrap().collect();
assert_eq!(posts.len(), 1);
ray.close().unwrap();
}
#[test]
fn test_count_nodes_by_type() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
ray.create_node("User", "alice", HashMap::new()).unwrap();
ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.create_node("Post", "post1", HashMap::new()).unwrap();
assert_eq!(ray.count_nodes_by_type("User").unwrap(), 2);
assert_eq!(ray.count_nodes_by_type("Post").unwrap(), 1);
assert_eq!(ray.count_nodes(), 3);
ray.close().unwrap();
}
#[test]
fn test_all_edges() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let post = ray.create_node("Post", "post1", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray.link(alice.id, "AUTHORED", post.id).unwrap();
let all_edges: Vec<_> = ray.all_edges(None).unwrap().collect();
assert_eq!(all_edges.len(), 2);
let follows_edges: Vec<_> = ray.all_edges(Some("FOLLOWS")).unwrap().collect();
assert_eq!(follows_edges.len(), 1);
assert_eq!(follows_edges[0].src, alice.id);
assert_eq!(follows_edges[0].dst, bob.id);
ray.close().unwrap();
}
#[test]
fn test_count_edges_by_type() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let post = ray.create_node("Post", "post1", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray.link(alice.id, "AUTHORED", post.id).unwrap();
assert_eq!(ray.count_edges_by_type("FOLLOWS").unwrap(), 1);
assert_eq!(ray.count_edges_by_type("AUTHORED").unwrap(), 1);
assert_eq!(ray.count_edges(), 2);
ray.close().unwrap();
}
#[test]
fn test_from_traversal() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let charlie = ray.create_node("User", "charlie", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray.link(bob.id, "FOLLOWS", charlie.id).unwrap();
let friends = ray.from(alice.id).out(Some("FOLLOWS")).unwrap().to_vec();
assert_eq!(friends, vec![bob.id]);
let friends_of_friends = ray
.from(alice.id)
.out(Some("FOLLOWS"))
.unwrap()
.out(Some("FOLLOWS"))
.unwrap()
.to_vec();
assert_eq!(friends_of_friends, vec![charlie.id]);
ray.close().unwrap();
}
#[test]
fn test_traversal_first() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
let first = ray
.from(alice.id)
.out(Some("FOLLOWS"))
.unwrap()
.first_node();
assert_eq!(first, Some(bob.id));
let no_result = ray.from(bob.id).out(Some("FOLLOWS")).unwrap().first_node();
assert_eq!(no_result, None);
ray.close().unwrap();
}
#[test]
fn test_traversal_count() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let charlie = ray.create_node("User", "charlie", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray.link(alice.id, "FOLLOWS", charlie.id).unwrap();
let count = ray.from(alice.id).out(Some("FOLLOWS")).unwrap().count();
assert_eq!(count, 2);
ray.close().unwrap();
}
#[test]
fn test_shortest_path() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let charlie = ray.create_node("User", "charlie", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray.link(bob.id, "FOLLOWS", charlie.id).unwrap();
let path = ray
.shortest_path(alice.id, charlie.id)
.via("FOLLOWS")
.unwrap()
.find();
assert!(path.found);
assert_eq!(path.path, vec![alice.id, bob.id, charlie.id]);
assert_eq!(path.edges.len(), 2);
ray.close().unwrap();
}
#[test]
fn test_shortest_path_not_found() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let path = ray
.shortest_path(alice.id, bob.id)
.via("FOLLOWS")
.unwrap()
.find();
assert!(!path.found);
assert!(path.path.is_empty());
ray.close().unwrap();
}
#[test]
fn test_has_path() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let charlie = ray.create_node("User", "charlie", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
assert!(ray.has_path(alice.id, bob.id, Some("FOLLOWS")).unwrap());
assert!(!ray.has_path(alice.id, charlie.id, Some("FOLLOWS")).unwrap());
assert!(!ray.has_path(bob.id, alice.id, Some("FOLLOWS")).unwrap());
ray.close().unwrap();
}
#[test]
fn test_reachable_from() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let charlie = ray.create_node("User", "charlie", HashMap::new()).unwrap();
let dave = ray.create_node("User", "dave", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray.link(bob.id, "FOLLOWS", charlie.id).unwrap();
ray.link(charlie.id, "FOLLOWS", dave.id).unwrap();
let reachable = ray.reachable_from(alice.id, 2, Some("FOLLOWS")).unwrap();
assert!(reachable.contains(&bob.id));
assert!(reachable.contains(&charlie.id));
assert!(!reachable.contains(&dave.id));
let reachable_3 = ray.reachable_from(alice.id, 3, Some("FOLLOWS")).unwrap();
assert!(reachable_3.contains(&dave.id));
ray.close().unwrap();
}
#[test]
fn test_k_shortest_paths() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let charlie = ray.create_node("User", "charlie", HashMap::new()).unwrap();
let dave = ray.create_node("User", "dave", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray.link(alice.id, "FOLLOWS", charlie.id).unwrap();
ray.link(bob.id, "FOLLOWS", dave.id).unwrap();
ray.link(charlie.id, "FOLLOWS", dave.id).unwrap();
let paths = ray
.shortest_path(alice.id, dave.id)
.via("FOLLOWS")
.unwrap()
.find_k_shortest(2);
assert_eq!(paths.len(), 2);
assert!(paths[0].found);
assert!(paths[1].found);
assert_eq!(paths[0].edges.len(), 2);
assert_eq!(paths[1].edges.len(), 2);
ray.close().unwrap();
}
#[test]
fn test_batch_create_nodes() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let results = ray
.batch(vec![
BatchOp::CreateNode {
node_type: "User".into(),
key_suffix: "alice".into(),
props: HashMap::new(),
},
BatchOp::CreateNode {
node_type: "User".into(),
key_suffix: "bob".into(),
props: HashMap::new(),
},
BatchOp::CreateNode {
node_type: "Post".into(),
key_suffix: "post1".into(),
props: HashMap::new(),
},
])
.unwrap();
assert_eq!(results.len(), 3);
assert_eq!(ray.count_nodes(), 3);
assert!(ray.get("User", "alice").unwrap().is_some());
assert!(ray.get("User", "bob").unwrap().is_some());
assert!(ray.get("Post", "post1").unwrap().is_some());
ray.close().unwrap();
}
#[test]
fn test_batch_create_and_link() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let results = ray
.batch(vec![
BatchOp::CreateNode {
node_type: "User".into(),
key_suffix: "alice".into(),
props: HashMap::new(),
},
BatchOp::CreateNode {
node_type: "User".into(),
key_suffix: "bob".into(),
props: HashMap::new(),
},
])
.unwrap();
let alice_id = match &results[0] {
BatchResult::NodeCreated(node_ref) => node_ref.id,
_ => panic!("Expected NodeCreated"),
};
let bob_id = match &results[1] {
BatchResult::NodeCreated(node_ref) => node_ref.id,
_ => panic!("Expected NodeCreated"),
};
ray
.batch(vec![BatchOp::Link {
src: alice_id,
edge_type: "FOLLOWS".into(),
dst: bob_id,
}])
.unwrap();
assert!(ray.has_edge(alice_id, "FOLLOWS", bob_id).unwrap());
ray.close().unwrap();
}
#[test]
fn test_batch_set_properties() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let user = ray.create_node("User", "alice", HashMap::new()).unwrap();
ray
.batch(vec![
BatchOp::SetProp {
node_id: user.id,
prop_name: "name".into(),
value: PropValue::String("Alice".into()),
},
BatchOp::SetProp {
node_id: user.id,
prop_name: "age".into(),
value: PropValue::I64(30),
},
])
.unwrap();
assert_eq!(
ray.get_prop(user.id, "name"),
Some(PropValue::String("Alice".into()))
);
assert_eq!(ray.get_prop(user.id, "age"), Some(PropValue::I64(30)));
ray.close().unwrap();
}
#[test]
fn test_batch_mixed_operations() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
let results = ray
.batch(vec![
BatchOp::SetProp {
node_id: alice.id,
prop_name: "name".into(),
value: PropValue::String("Alice".into()),
},
BatchOp::CreateNode {
node_type: "User".into(),
key_suffix: "charlie".into(),
props: HashMap::new(),
},
BatchOp::Unlink {
src: alice.id,
edge_type: "FOLLOWS".into(),
dst: bob.id,
},
])
.unwrap();
assert_eq!(results.len(), 3);
assert_eq!(
ray.get_prop(alice.id, "name"),
Some(PropValue::String("Alice".into()))
);
assert!(ray.get("User", "charlie").unwrap().is_some());
assert!(!ray.has_edge(alice.id, "FOLLOWS", bob.id).unwrap());
ray.close().unwrap();
}
#[test]
fn test_batch_delete_operations() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
let results = ray
.batch(vec![
BatchOp::Unlink {
src: alice.id,
edge_type: "FOLLOWS".into(),
dst: bob.id,
},
BatchOp::DeleteNode { node_id: bob.id },
])
.unwrap();
match &results[0] {
BatchResult::EdgeRemoved(removed) => assert!(*removed),
_ => panic!("Expected EdgeRemoved"),
}
match &results[1] {
BatchResult::NodeDeleted(deleted) => assert!(*deleted),
_ => panic!("Expected NodeDeleted"),
}
assert!(!ray.exists(bob.id));
ray.close().unwrap();
}
#[test]
fn test_transaction_basic() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let (alice, bob) = ray
.transaction(|ctx| {
let alice = ctx.create_node("User", "alice", HashMap::new())?;
let bob = ctx.create_node("User", "bob", HashMap::new())?;
ctx.link(alice.id, "FOLLOWS", bob.id)?;
Ok((alice, bob))
})
.unwrap();
assert!(ray.exists(alice.id));
assert!(ray.exists(bob.id));
assert!(ray.has_edge(alice.id, "FOLLOWS", bob.id).unwrap());
ray.close().unwrap();
}
#[test]
fn test_transaction_with_properties() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray
.transaction(|ctx| {
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Alice".into()));
let alice = ctx.create_node("User", "alice", props)?;
ctx.set_prop(alice.id, "age", PropValue::I64(30))?;
Ok(alice)
})
.unwrap();
assert_eq!(
ray.get_prop(alice.id, "name"),
Some(PropValue::String("Alice".into()))
);
assert_eq!(ray.get_prop(alice.id, "age"), Some(PropValue::I64(30)));
ray.close().unwrap();
}
#[test]
fn test_transaction_rollback_on_error() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let result: Result<()> = ray.transaction(|ctx| {
ctx.create_node("User", "alice", HashMap::new())?;
ctx.create_node("UnknownType", "bob", HashMap::new())?;
Ok(())
});
assert!(result.is_err());
ray.close().unwrap();
}
#[test]
fn test_transaction_read_operations() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
ray
.set_prop(alice.id, "name", PropValue::String("Alice".into()))
.unwrap();
let name = ray
.transaction(|ctx| {
let existing = ctx.get("User", "alice")?;
assert!(existing.is_some());
let name = ctx.get_prop(alice.id, "name")?;
assert!(ctx.exists(alice.id));
ctx.create_node("User", "bob", HashMap::new())?;
Ok(name)
})
.unwrap();
assert_eq!(name, Some(PropValue::String("Alice".into())));
assert!(ray.get("User", "bob").unwrap().is_some());
ray.close().unwrap();
}
#[test]
fn test_transaction_edge_operations() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let charlie = ray.create_node("User", "charlie", HashMap::new()).unwrap();
ray
.transaction(|ctx| {
ctx.link(alice.id, "FOLLOWS", bob.id)?;
ctx.link(bob.id, "FOLLOWS", charlie.id)?;
Ok(())
})
.unwrap();
assert!(ray.has_edge(alice.id, "FOLLOWS", bob.id).unwrap());
assert!(ray.has_edge(bob.id, "FOLLOWS", charlie.id).unwrap());
assert!(!ray.has_edge(alice.id, "FOLLOWS", charlie.id).unwrap());
ray
.transaction(|ctx| {
ctx.unlink(alice.id, "FOLLOWS", bob.id)?;
Ok(())
})
.unwrap();
assert!(!ray.has_edge(alice.id, "FOLLOWS", bob.id).unwrap());
assert!(ray.has_edge(bob.id, "FOLLOWS", charlie.id).unwrap());
ray.close().unwrap();
}
#[test]
fn test_tx_builder() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let results = ray
.tx()
.create_node("User", "alice", HashMap::new())
.create_node("User", "bob", HashMap::new())
.execute(&mut ray)
.unwrap();
assert_eq!(results.len(), 2);
let alice_id = match &results[0] {
BatchResult::NodeCreated(node_ref) => node_ref.id,
_ => panic!("Expected NodeCreated"),
};
let bob_id = match &results[1] {
BatchResult::NodeCreated(node_ref) => node_ref.id,
_ => panic!("Expected NodeCreated"),
};
ray
.tx()
.link(alice_id, "FOLLOWS", bob_id)
.set_prop(alice_id, "name", PropValue::String("Alice".into()))
.execute(&mut ray)
.unwrap();
assert!(ray.has_edge(alice_id, "FOLLOWS", bob_id).unwrap());
assert_eq!(
ray.get_prop(alice_id, "name"),
Some(PropValue::String("Alice".into()))
);
ray.close().unwrap();
}
#[test]
fn test_tx_builder_into_ops() {
let ops = TxBuilder::default()
.create_node("User", "alice", HashMap::new())
.link(1, "FOLLOWS", 2)
.set_prop(1, "name", PropValue::String("Test".into()))
.into_ops();
assert_eq!(ops.len(), 3);
}
#[test]
fn test_link_with_props() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let mut props = HashMap::new();
props.insert("weight".to_string(), PropValue::F64(0.8));
props.insert("since".to_string(), PropValue::String("2024".into()));
ray
.link_with_props(alice.id, "FOLLOWS", bob.id, props)
.unwrap();
assert!(ray.has_edge(alice.id, "FOLLOWS", bob.id).unwrap());
let weight = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.unwrap();
assert_eq!(weight, Some(PropValue::F64(0.8)));
let since = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.unwrap();
assert_eq!(since, Some(PropValue::String("2024".into())));
ray.close().unwrap();
}
#[test]
fn test_set_edge_prop() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray
.set_edge_prop(alice.id, "FOLLOWS", bob.id, "weight", PropValue::F64(0.5))
.unwrap();
let weight = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.unwrap();
assert_eq!(weight, Some(PropValue::F64(0.5)));
ray
.set_edge_prop(alice.id, "FOLLOWS", bob.id, "weight", PropValue::F64(0.9))
.unwrap();
let new_weight = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.unwrap();
assert_eq!(new_weight, Some(PropValue::F64(0.9)));
ray.close().unwrap();
}
#[test]
fn test_get_edge_props() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let mut props = HashMap::new();
props.insert("weight".to_string(), PropValue::F64(0.7));
props.insert("type".to_string(), PropValue::String("friend".into()));
ray
.link_with_props(alice.id, "FOLLOWS", bob.id, props)
.unwrap();
let all_props = ray.get_edge_props(alice.id, "FOLLOWS", bob.id).unwrap();
assert!(all_props.is_some());
let all_props = all_props.unwrap();
assert_eq!(all_props.get("weight"), Some(&PropValue::F64(0.7)));
assert_eq!(
all_props.get("type"),
Some(&PropValue::String("friend".into()))
);
ray.close().unwrap();
}
#[test]
fn test_del_edge_prop() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray
.set_edge_prop(alice.id, "FOLLOWS", bob.id, "weight", PropValue::F64(0.5))
.unwrap();
let weight = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.unwrap();
assert_eq!(weight, Some(PropValue::F64(0.5)));
ray
.del_edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.unwrap();
let weight = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.unwrap();
assert_eq!(weight, None);
ray.close().unwrap();
}
#[test]
fn test_edge_prop_nonexistent_edge() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray
.set_edge_prop(alice.id, "FOLLOWS", bob.id, "weight", PropValue::F64(0.5))
.ok();
let _props = ray.get_edge_props(alice.id, "FOLLOWS", bob.id).unwrap();
let charlie = ray.create_node("User", "charlie", HashMap::new()).unwrap();
let props2 = ray.get_edge_props(alice.id, "FOLLOWS", charlie.id).unwrap();
assert!(props2.is_none());
ray.close().unwrap();
}
#[test]
fn test_update_edge_builder() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray
.update_edge(alice.id, "FOLLOWS", bob.id)
.unwrap()
.set("weight", PropValue::F64(0.9))
.set("since", PropValue::String("2024".into()))
.execute()
.unwrap();
let weight = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.unwrap();
assert_eq!(weight, Some(PropValue::F64(0.9)));
let since = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.unwrap();
assert_eq!(since, Some(PropValue::String("2024".into())));
ray
.update_edge(alice.id, "FOLLOWS", bob.id)
.unwrap()
.set("weight", PropValue::F64(0.5))
.unset("since")
.execute()
.unwrap();
let weight = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.unwrap();
assert_eq!(weight, Some(PropValue::F64(0.5)));
let since = ray
.get_edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.unwrap();
assert_eq!(since, None);
ray.close().unwrap();
}
#[test]
fn test_update_edge_builder_set_all() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
let mut props = HashMap::new();
props.insert("weight".to_string(), PropValue::F64(0.8));
props.insert("type".to_string(), PropValue::String("close_friend".into()));
ray
.update_edge(alice.id, "FOLLOWS", bob.id)
.unwrap()
.set_all(props)
.execute()
.unwrap();
let all_props = ray
.get_edge_props(alice.id, "FOLLOWS", bob.id)
.unwrap()
.unwrap();
assert_eq!(all_props.get("weight"), Some(&PropValue::F64(0.8)));
assert_eq!(
all_props.get("type"),
Some(&PropValue::String("close_friend".into()))
);
ray.close().unwrap();
}
#[test]
fn test_update_edge_builder_empty() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray
.update_edge(alice.id, "FOLLOWS", bob.id)
.unwrap()
.execute()
.unwrap();
ray.close().unwrap();
}
#[test]
fn test_insert_builder_returning() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Alice".into()));
props.insert("age".to_string(), PropValue::I64(30));
let alice = ray
.insert("User")
.unwrap()
.values("alice", props)
.unwrap()
.returning()
.unwrap();
assert!(alice.id > 0);
assert_eq!(alice.key, Some("user:alice".to_string()));
assert_eq!(alice.node_type, "User");
let name = ray.get_prop(alice.id, "name");
assert_eq!(name, Some(PropValue::String("Alice".into())));
let age = ray.get_prop(alice.id, "age");
assert_eq!(age, Some(PropValue::I64(30)));
ray.close().unwrap();
}
#[test]
fn test_insert_builder_execute() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Bob".into()));
ray
.insert("User")
.unwrap()
.values("bob", props)
.unwrap()
.execute()
.unwrap();
let bob = ray.get("User", "bob").unwrap();
assert!(bob.is_some());
let bob = bob.unwrap();
let name = ray.get_prop(bob.id, "name");
assert_eq!(name, Some(PropValue::String("Bob".into())));
ray.close().unwrap();
}
#[test]
fn test_insert_builder_values_many() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut alice_props = HashMap::new();
alice_props.insert("name".to_string(), PropValue::String("Alice".into()));
let mut bob_props = HashMap::new();
bob_props.insert("name".to_string(), PropValue::String("Bob".into()));
let mut charlie_props = HashMap::new();
charlie_props.insert("name".to_string(), PropValue::String("Charlie".into()));
let users = ray
.insert("User")
.unwrap()
.values_many(vec![
("alice", alice_props),
("bob", bob_props),
("charlie", charlie_props),
])
.unwrap()
.returning()
.unwrap();
assert_eq!(users.len(), 3);
assert_eq!(users[0].key, Some("user:alice".to_string()));
assert_eq!(users[1].key, Some("user:bob".to_string()));
assert_eq!(users[2].key, Some("user:charlie".to_string()));
assert_eq!(ray.count_nodes(), 3);
ray.close().unwrap();
}
#[test]
fn test_insert_builder_empty_values_many() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let users = ray
.insert("User")
.unwrap()
.values_many(vec![])
.unwrap()
.returning()
.unwrap();
assert_eq!(users.len(), 0);
assert_eq!(ray.count_nodes(), 0);
ray.close().unwrap();
}
#[test]
fn test_check_empty_database() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let ray = Ray::open(temp_dir.path(), options).unwrap();
let result = ray.check().unwrap();
assert!(result.valid);
assert!(result.errors.is_empty());
assert!(result.warnings.iter().any(|w| w.contains("No snapshot")));
ray.close().unwrap();
}
#[test]
fn test_check_valid_database() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
let charlie = ray.create_node("User", "charlie", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
ray.link(bob.id, "FOLLOWS", charlie.id).unwrap();
ray.link(charlie.id, "FOLLOWS", alice.id).unwrap();
let result = ray.check().unwrap();
assert!(
result.valid,
"Expected valid database, got errors: {:?}",
result.errors
);
assert!(result.errors.is_empty());
ray.close().unwrap();
}
#[test]
fn test_check_with_properties() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Alice".into()));
props.insert("age".to_string(), PropValue::I64(30));
let alice = ray.create_node("User", "alice", props).unwrap();
let mut props2 = HashMap::new();
props2.insert("name".to_string(), PropValue::String("Bob".into()));
let bob = ray.create_node("User", "bob", props2).unwrap();
let mut edge_props = HashMap::new();
edge_props.insert("weight".to_string(), PropValue::F64(0.9));
ray
.link_with_props(alice.id, "FOLLOWS", bob.id, edge_props)
.unwrap();
let result = ray.check().unwrap();
assert!(
result.valid,
"Expected valid database, got errors: {:?}",
result.errors
);
assert!(result.errors.is_empty());
ray.close().unwrap();
}
#[test]
fn test_update_node_by_ref() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Alice".into()));
props.insert("age".to_string(), PropValue::I64(30));
let alice = ray.create_node("User", "alice", props).unwrap();
ray
.update(&alice)
.unwrap()
.set("name", PropValue::String("Alice Updated".into()))
.set("age", PropValue::I64(31))
.execute()
.unwrap();
let name = ray.get_prop(alice.id, "name");
assert_eq!(name, Some(PropValue::String("Alice Updated".into())));
let age = ray.get_prop(alice.id, "age");
assert_eq!(age, Some(PropValue::I64(31)));
ray.close().unwrap();
}
#[test]
fn test_update_node_by_key() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Bob".into()));
ray.create_node("User", "bob", props).unwrap();
ray
.update_by_key("User", "bob")
.unwrap()
.set("name", PropValue::String("Bob Updated".into()))
.set("age", PropValue::I64(25))
.execute()
.unwrap();
let bob = ray.get("User", "bob").unwrap().unwrap();
let name = ray.get_prop(bob.id, "name");
assert_eq!(name, Some(PropValue::String("Bob Updated".into())));
let age = ray.get_prop(bob.id, "age");
assert_eq!(age, Some(PropValue::I64(25)));
ray.close().unwrap();
}
#[test]
fn test_update_node_by_id() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Charlie".into()));
let charlie = ray.create_node("User", "charlie", props).unwrap();
ray
.update_by_id(charlie.id)
.unwrap()
.set("name", PropValue::String("Charlie Updated".into()))
.execute()
.unwrap();
let name = ray.get_prop(charlie.id, "name");
assert_eq!(name, Some(PropValue::String("Charlie Updated".into())));
ray.close().unwrap();
}
#[test]
fn test_update_node_unset() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Dave".into()));
props.insert("age".to_string(), PropValue::I64(40));
let dave = ray.create_node("User", "dave", props).unwrap();
assert!(ray.get_prop(dave.id, "age").is_some());
ray
.update(&dave)
.unwrap()
.set("name", PropValue::String("Dave Updated".into()))
.unset("age")
.execute()
.unwrap();
let name = ray.get_prop(dave.id, "name");
assert_eq!(name, Some(PropValue::String("Dave Updated".into())));
let age = ray.get_prop(dave.id, "age");
assert_eq!(age, None);
ray.close().unwrap();
}
#[test]
fn test_update_node_set_all() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let eve = ray.create_node("User", "eve", HashMap::new()).unwrap();
let mut updates = HashMap::new();
updates.insert("name".to_string(), PropValue::String("Eve".into()));
updates.insert("age".to_string(), PropValue::I64(28));
ray
.update(&eve)
.unwrap()
.set_all(updates)
.execute()
.unwrap();
let name = ray.get_prop(eve.id, "name");
assert_eq!(name, Some(PropValue::String("Eve".into())));
let age = ray.get_prop(eve.id, "age");
assert_eq!(age, Some(PropValue::I64(28)));
ray.close().unwrap();
}
#[test]
fn test_update_node_nonexistent() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let result = ray.update_by_id(999999);
assert!(result.is_err());
let result = ray.update_by_key("User", "nonexistent");
assert!(result.is_err());
ray.close().unwrap();
}
#[test]
fn test_update_node_empty() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Frank".into()));
let frank = ray.create_node("User", "frank", props).unwrap();
ray.update(&frank).unwrap().execute().unwrap();
let name = ray.get_prop(frank.id, "name");
assert_eq!(name, Some(PropValue::String("Frank".into())));
ray.close().unwrap();
}
#[test]
fn test_describe() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
let desc = ray.describe();
assert!(desc.contains("KiteDB at"));
assert!(desc.contains("format"));
assert!(desc.contains("User"));
assert!(desc.contains("FOLLOWS"));
assert!(desc.contains("Nodes:"));
assert!(desc.contains("Edges:"));
ray.close().unwrap();
}
#[test]
fn test_stats() {
let temp_dir = tempdir().unwrap();
let options = create_test_schema();
let mut ray = Ray::open(temp_dir.path(), options).unwrap();
let alice = ray.create_node("User", "alice", HashMap::new()).unwrap();
let bob = ray.create_node("User", "bob", HashMap::new()).unwrap();
ray.link(alice.id, "FOLLOWS", bob.id).unwrap();
let stats = ray.stats();
assert!(stats.snapshot_nodes >= 2);
assert!(stats.snapshot_edges >= 1);
assert!(stats.delta_nodes_created >= 2);
assert!(stats.delta_edges_added >= 1);
ray.close().unwrap();
}
}