use crate::core::single_file::{
close_single_file, is_single_file_path, open_single_file, single_file_extension, FullEdge,
SingleFileDB, SingleFileOpenOptions, SyncMode,
};
use crate::error::{KiteError, Result};
use crate::types::*;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
struct NodeOpts {
key: Option<String>,
labels: Option<Vec<LabelId>>,
props: Option<Vec<(PropKeyId, PropValue)>>,
}
impl NodeOpts {
fn new() -> Self {
Self::default()
}
fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
#[allow(dead_code)]
fn with_label(mut self, label: LabelId) -> Self {
self.labels.get_or_insert_with(Vec::new).push(label);
self
}
#[allow(dead_code)]
fn with_prop(mut self, key: PropKeyId, value: PropValue) -> Self {
self.props.get_or_insert_with(Vec::new).push((key, value));
self
}
}
struct TxHandle<'a> {
db: &'a SingleFileDB,
finished: bool,
owns_tx: bool,
}
impl<'a> TxHandle<'a> {
fn new(db: &'a SingleFileDB, owns_tx: bool) -> Self {
Self {
db,
finished: false,
owns_tx,
}
}
}
impl<'a> Drop for TxHandle<'a> {
fn drop(&mut self) {
if !self.finished {
if self.owns_tx {
let _ = self.db.rollback();
}
self.finished = true;
}
}
}
fn begin_tx(db: &SingleFileDB) -> Result<TxHandle<'_>> {
if db.has_transaction() {
db.require_write_tx()?;
return Ok(TxHandle::new(db, false));
}
db.begin(false)?;
Ok(TxHandle::new(db, true))
}
fn commit(handle: &mut TxHandle) -> Result<()> {
if handle.owns_tx {
handle.db.commit()?;
}
handle.finished = true;
Ok(())
}
fn rollback(handle: &mut TxHandle) -> Result<()> {
if handle.owns_tx {
handle.db.rollback()?;
}
handle.finished = true;
Ok(())
}
#[derive(Debug, Clone, Default)]
struct ListEdgesOptions {
pub etype: Option<ETypeId>,
}
fn create_node(handle: &mut TxHandle, opts: NodeOpts) -> Result<NodeId> {
let node_id = handle.db.create_node(opts.key.as_deref())?;
if let Some(labels) = opts.labels {
for label_id in labels {
handle.db.add_node_label(node_id, label_id)?;
}
}
if let Some(props) = opts.props {
for (key_id, value) in props {
handle.db.set_node_prop(node_id, key_id, value)?;
}
}
Ok(node_id)
}
fn create_node_with_id(handle: &mut TxHandle, node_id: NodeId, opts: NodeOpts) -> Result<NodeId> {
let node_id = handle
.db
.create_node_with_id(node_id, opts.key.as_deref())?;
if let Some(labels) = opts.labels {
for label_id in labels {
handle.db.add_node_label(node_id, label_id)?;
}
}
if let Some(props) = opts.props {
for (key_id, value) in props {
handle.db.set_node_prop(node_id, key_id, value)?;
}
}
Ok(node_id)
}
fn delete_node(handle: &mut TxHandle, node_id: NodeId) -> Result<bool> {
if !handle.db.node_exists(node_id) {
return Ok(false);
}
handle.db.delete_node(node_id)?;
Ok(true)
}
fn node_exists(handle: &TxHandle, node_id: NodeId) -> bool {
handle.db.node_exists(node_id)
}
fn node_exists_db(db: &SingleFileDB, node_id: NodeId) -> bool {
db.node_exists(node_id)
}
fn node_by_key(handle: &TxHandle, key: &str) -> Option<NodeId> {
handle.db.node_by_key(key)
}
fn node_by_key_db(db: &SingleFileDB, key: &str) -> Option<NodeId> {
db.node_by_key(key)
}
fn node_prop(handle: &TxHandle, node_id: NodeId, key_id: PropKeyId) -> Option<PropValue> {
handle.db.node_prop(node_id, key_id)
}
fn node_prop_db(db: &SingleFileDB, node_id: NodeId, key_id: PropKeyId) -> Option<PropValue> {
db.node_prop(node_id, key_id)
}
fn set_node_prop(
handle: &mut TxHandle,
node_id: NodeId,
key_id: PropKeyId,
value: PropValue,
) -> Result<()> {
handle.db.set_node_prop(node_id, key_id, value)
}
fn del_node_prop(handle: &mut TxHandle, node_id: NodeId, key_id: PropKeyId) -> Result<()> {
handle.db.delete_node_prop(node_id, key_id)
}
fn upsert_node_with_props<I>(handle: &mut TxHandle, key: &str, props: I) -> Result<(NodeId, bool)>
where
I: IntoIterator<Item = (PropKeyId, Option<PropValue>)>,
{
let (node_id, created) = match handle.db.node_by_key(key) {
Some(existing) => (existing, false),
None => (create_node(handle, NodeOpts::new().with_key(key))?, true),
};
for (key_id, value_opt) in props {
match value_opt {
Some(value) => set_node_prop(handle, node_id, key_id, value)?,
None => del_node_prop(handle, node_id, key_id)?,
}
}
Ok((node_id, created))
}
fn upsert_node_by_id_with_props<I>(
handle: &mut TxHandle,
node_id: NodeId,
opts: NodeOpts,
props: I,
) -> Result<(NodeId, bool)>
where
I: IntoIterator<Item = (PropKeyId, Option<PropValue>)>,
{
let created = if handle.db.node_exists(node_id) {
false
} else {
create_node_with_id(handle, node_id, opts)?;
true
};
for (key_id, value_opt) in props {
match value_opt {
Some(value) => set_node_prop(handle, node_id, key_id, value)?,
None => del_node_prop(handle, node_id, key_id)?,
}
}
Ok((node_id, created))
}
fn add_edge(handle: &mut TxHandle, src: NodeId, etype: ETypeId, dst: NodeId) -> Result<()> {
handle.db.add_edge(src, etype, dst)
}
fn delete_edge(handle: &mut TxHandle, src: NodeId, etype: ETypeId, dst: NodeId) -> Result<bool> {
if !handle.db.edge_exists(src, etype, dst) {
return Ok(false);
}
handle.db.delete_edge(src, etype, dst)?;
Ok(true)
}
fn edge_exists(handle: &TxHandle, src: NodeId, etype: ETypeId, dst: NodeId) -> bool {
handle.db.edge_exists(src, etype, dst)
}
fn edge_exists_db(db: &SingleFileDB, src: NodeId, etype: ETypeId, dst: NodeId) -> bool {
db.edge_exists(src, etype, dst)
}
fn neighbors_out_db(db: &SingleFileDB, node_id: NodeId, etype: Option<ETypeId>) -> Vec<NodeId> {
match etype {
Some(filter) => db.out_neighbors(node_id, filter),
None => db
.out_edges(node_id)
.into_iter()
.map(|(_, dst)| dst)
.collect(),
}
}
fn neighbors_in_db(db: &SingleFileDB, node_id: NodeId, etype: Option<ETypeId>) -> Vec<NodeId> {
match etype {
Some(filter) => db.in_neighbors(node_id, filter),
None => db
.in_edges(node_id)
.into_iter()
.map(|(_, src)| src)
.collect(),
}
}
fn edge_prop_db(
db: &SingleFileDB,
src: NodeId,
etype: ETypeId,
dst: NodeId,
key_id: PropKeyId,
) -> Option<PropValue> {
db.edge_prop(src, etype, dst, key_id)
}
fn edge_props_db(
db: &SingleFileDB,
src: NodeId,
etype: ETypeId,
dst: NodeId,
) -> Option<HashMap<PropKeyId, PropValue>> {
db.edge_props(src, etype, dst)
}
fn set_edge_prop(
handle: &mut TxHandle,
src: NodeId,
etype: ETypeId,
dst: NodeId,
key_id: PropKeyId,
value: PropValue,
) -> Result<()> {
handle.db.set_edge_prop(src, etype, dst, key_id, value)
}
fn del_edge_prop(
handle: &mut TxHandle,
src: NodeId,
etype: ETypeId,
dst: NodeId,
key_id: PropKeyId,
) -> Result<()> {
handle.db.delete_edge_prop(src, etype, dst, key_id)
}
fn upsert_edge_with_props<I>(
handle: &mut TxHandle,
src: NodeId,
etype: ETypeId,
dst: NodeId,
props: I,
) -> Result<bool>
where
I: IntoIterator<Item = (PropKeyId, Option<PropValue>)>,
{
handle.db.upsert_edge_with_props(src, etype, dst, props)
}
fn list_nodes(db: &SingleFileDB) -> Vec<NodeId> {
db.list_nodes()
}
fn list_edges(db: &SingleFileDB, options: ListEdgesOptions) -> Vec<FullEdge> {
db.list_edges(options.etype)
}
fn count_nodes(db: &SingleFileDB) -> u64 {
db.count_nodes() as u64
}
fn count_edges(db: &SingleFileDB, etype_filter: Option<ETypeId>) -> u64 {
match etype_filter {
Some(etype) => db.count_edges_by_type(etype) as u64,
None => db.count_edges() as u64,
}
}
#[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 {
id: NodeId,
key: Option<String>,
node_type: Arc<str>,
}
impl NodeRef {
pub fn new(id: NodeId, key: Option<String>, node_type: impl Into<Arc<str>>) -> Self {
Self {
id,
key,
node_type: node_type.into(),
}
}
pub fn id(&self) -> NodeId {
self.id
}
pub fn key(&self) -> Option<&str> {
self.key.as_deref()
}
pub fn node_type(&self) -> &str {
&self.node_type
}
pub fn into_parts(self) -> (NodeId, Option<String>, Arc<str>) {
(self.id, self.key, self.node_type)
}
}
#[derive(Debug, Clone)]
pub struct KiteOptions {
pub nodes: Vec<NodeDef>,
pub edges: Vec<EdgeDef>,
pub read_only: bool,
pub create_if_missing: bool,
pub sync_mode: SyncMode,
pub group_commit_enabled: bool,
pub group_commit_window_ms: u64,
pub mvcc: bool,
pub mvcc_gc_interval_ms: Option<u64>,
pub mvcc_retention_ms: Option<u64>,
pub mvcc_max_chain_depth: Option<usize>,
pub wal_size: Option<usize>,
pub checkpoint_threshold: Option<f64>,
}
impl KiteOptions {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
read_only: false,
create_if_missing: true,
sync_mode: SyncMode::Full,
group_commit_enabled: false,
group_commit_window_ms: 2,
mvcc: false,
mvcc_gc_interval_ms: None,
mvcc_retention_ms: None,
mvcc_max_chain_depth: None,
wal_size: None,
checkpoint_threshold: None,
}
}
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 fn sync_mode(mut self, mode: SyncMode) -> Self {
self.sync_mode = mode;
self
}
pub fn group_commit_enabled(mut self, value: bool) -> Self {
self.group_commit_enabled = value;
self
}
pub fn group_commit_window_ms(mut self, value: u64) -> Self {
self.group_commit_window_ms = value;
self
}
pub fn sync_normal(mut self) -> Self {
self.sync_mode = SyncMode::Normal;
self
}
pub fn sync_off(mut self) -> Self {
self.sync_mode = SyncMode::Off;
self
}
pub fn mvcc(mut self, value: bool) -> Self {
self.mvcc = value;
self
}
pub fn mvcc_gc_interval_ms(mut self, value: u64) -> Self {
self.mvcc_gc_interval_ms = Some(value);
self
}
pub fn mvcc_retention_ms(mut self, value: u64) -> Self {
self.mvcc_retention_ms = Some(value);
self
}
pub fn mvcc_max_chain_depth(mut self, value: usize) -> Self {
self.mvcc_max_chain_depth = Some(value);
self
}
pub fn wal_size(mut self, value: usize) -> Self {
self.wal_size = Some(value);
self
}
pub fn wal_size_mb(mut self, value: usize) -> Self {
self.wal_size = Some(value.saturating_mul(1024 * 1024));
self
}
pub fn checkpoint_threshold(mut self, value: f64) -> Self {
self.checkpoint_threshold = Some(value.clamp(0.0, 1.0));
self
}
}
impl Default for KiteOptions {
fn default() -> Self {
Self::new()
}
}
pub fn kite<P: AsRef<Path>>(path: P, options: KiteOptions) -> Result<Kite> {
Kite::open(path, options)
}
pub struct Kite {
db: SingleFileDB,
nodes: HashMap<String, NodeDef>,
edges: HashMap<String, EdgeDef>,
key_prefix_to_node: HashMap<String, String>,
}
impl Kite {
pub fn open<P: AsRef<Path>>(path: P, options: KiteOptions) -> Result<Self> {
let path = path.as_ref();
if path.exists() && path.is_dir() {
return Err(KiteError::InvalidPath(
"Directory-format databases are no longer supported; use a .kitedb file path".to_string(),
));
}
let mut db_path = PathBuf::from(path);
if db_path.extension().is_some() {
if !is_single_file_path(&db_path) {
let ext = db_path
.extension()
.map(|value| value.to_string_lossy())
.unwrap_or_else(|| "".into());
return Err(KiteError::InvalidPath(format!(
"Invalid database extension '.{ext}'. Single-file databases must use {} (or pass a path without an extension).",
single_file_extension()
)));
}
} else {
db_path = PathBuf::from(format!("{}{}", path.display(), single_file_extension()));
}
let mut db_options = SingleFileOpenOptions::new()
.read_only(options.read_only)
.create_if_missing(options.create_if_missing)
.sync_mode(options.sync_mode)
.group_commit_enabled(options.group_commit_enabled)
.group_commit_window_ms(options.group_commit_window_ms)
.mvcc(options.mvcc);
if let Some(v) = options.mvcc_gc_interval_ms {
db_options = db_options.mvcc_gc_interval_ms(v);
}
if let Some(v) = options.mvcc_retention_ms {
db_options = db_options.mvcc_retention_ms(v);
}
if let Some(v) = options.mvcc_max_chain_depth {
db_options = db_options.mvcc_max_chain_depth(v);
}
if let Some(v) = options.wal_size {
db_options = db_options.wal_size(v);
}
if let Some(v) = options.checkpoint_threshold {
db_options = db_options.checkpoint_threshold(v);
}
let db = open_single_file(&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.label_id_or_create(&node_def.name);
node_def.label_id = Some(label_id);
for prop_name in node_def.props.keys() {
let prop_key_id = db.propkey_id_or_create(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.etype_id_or_create(&edge_def.name);
edge_def.etype_id = Some(etype_id);
for prop_name in edge_def.props.keys() {
let prop_key_id = db.propkey_id_or_create(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(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?
.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<KiteInsertBuilder<'_>> {
let key_prefix = self
.nodes
.get(node_type)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?
.key_prefix
.clone();
Ok(KiteInsertBuilder {
ray: self,
node_type: node_type.to_string(),
key_prefix,
})
}
pub fn upsert(&mut self, node_type: &str) -> Result<KiteUpsertBuilder<'_>> {
let key_prefix = self
.nodes
.get(node_type)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?
.key_prefix
.clone();
Ok(KiteUpsertBuilder {
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(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?;
let full_key = node_def.key(key_suffix);
let node_id = 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 node_by_id(&self, node_id: NodeId) -> Result<Option<NodeRef>> {
let exists = node_exists_db(&self.db, node_id);
if exists {
let key = self.db.node_key(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 prop(&self, node_id: NodeId, prop_name: &str) -> Option<PropValue> {
let prop_key_id = self.db.propkey_id(prop_name)?;
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.propkey_id_or_create(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 set_props<I, K>(&mut self, node_id: NodeId, props: I) -> Result<()>
where
I: IntoIterator<Item = (K, PropValue)>,
K: AsRef<str>,
{
let mut iter = props.into_iter();
let Some((first_name, first_value)) = iter.next() else {
return Ok(());
};
let mut handle = begin_tx(&self.db)?;
let first_key_id = self.db.propkey_id_or_create(first_name.as_ref());
set_node_prop(&mut handle, node_id, first_key_id, first_value)?;
for (prop_name, value) in iter {
let prop_key_id = self.db.propkey_id_or_create(prop_name.as_ref());
set_node_prop(&mut handle, node_id, prop_key_id, value)?;
}
commit(&mut handle)?;
Ok(())
}
pub fn update(&mut self, node_ref: &NodeRef) -> Result<KiteUpdateNodeBuilder<'_>> {
let exists = {
let mut handle = begin_tx(&self.db)?;
let exists = node_exists(&handle, node_ref.id());
commit(&mut handle)?;
exists
};
if !exists {
return Err(KiteError::NodeNotFound(node_ref.id()));
}
Ok(KiteUpdateNodeBuilder {
ray: self,
node_id: node_ref.id(),
updates: HashMap::new(),
})
}
pub fn update_by_id(&mut self, node_id: NodeId) -> Result<KiteUpdateNodeBuilder<'_>> {
let exists = {
let mut handle = begin_tx(&self.db)?;
let exists = node_exists(&handle, node_id);
commit(&mut handle)?;
exists
};
if !exists {
return Err(KiteError::NodeNotFound(node_id));
}
Ok(KiteUpdateNodeBuilder {
ray: self,
node_id,
updates: HashMap::new(),
})
}
pub fn upsert_by_id(
&mut self,
node_type: &str,
node_id: NodeId,
) -> Result<KiteUpsertByIdBuilder<'_>> {
let node_def = self
.nodes
.get(node_type)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?
.clone();
Ok(KiteUpsertByIdBuilder {
ray: self,
node_id,
node_def,
updates: HashMap::new(),
})
}
pub fn update_by_key(
&mut self,
node_type: &str,
key_suffix: &str,
) -> Result<KiteUpdateNodeBuilder<'_>> {
let full_key = self
.nodes
.get(node_type)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?
.key(key_suffix);
let node_id = {
let mut handle = begin_tx(&self.db)?;
let node_id =
node_by_key(&handle, &full_key).ok_or_else(|| KiteError::KeyNotFound(full_key.clone()))?;
commit(&mut handle)?;
node_id
};
Ok(KiteUpdateNodeBuilder {
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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?
.clone();
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
let mut handle = begin_tx(&self.db)?;
if props.is_empty() {
add_edge(&mut handle, src, etype_id, dst)?;
} else {
let mut prop_pairs = Vec::with_capacity(props.len());
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.propkey_id_or_create(&prop_name)
};
prop_pairs.push((prop_key_id, value));
}
handle
.db
.add_edge_with_props(src, etype_id, dst, prop_pairs)?;
}
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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
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(|| KiteError::InvalidSchema(format!("Unknown edge type: {name}").into()))?;
edge_def.etype_id
}
None => None,
};
Ok(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(|| KiteError::InvalidSchema(format!("Unknown edge type: {name}").into()))?;
edge_def.etype_id
}
None => None,
};
let neighbors = neighbors_in_db(&self.db, node_id, etype_id);
Ok(neighbors)
}
pub fn 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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
let prop_key_id = match self.db.propkey_id(prop_name) {
Some(id) => id,
None => return Ok(None), };
Ok(edge_prop_db(&self.db, src, etype_id, dst, prop_key_id))
}
pub fn 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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
let props = 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.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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
let prop_key_id = self.db.propkey_id_or_create(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 set_edge_props(
&mut self,
src: NodeId,
edge_type: &str,
dst: NodeId,
props: HashMap<String, PropValue>,
) -> Result<()> {
if props.is_empty() {
return Ok(());
}
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
let mut prop_pairs = Vec::with_capacity(props.len());
for (prop_name, value) in props {
let prop_key_id = if let Some(&id) = edge_def.prop_key_ids.get(&prop_name) {
id
} else {
self.db.propkey_id_or_create(&prop_name)
};
prop_pairs.push((prop_key_id, value));
}
let mut handle = begin_tx(&self.db)?;
handle.db.set_edge_props(src, etype_id, dst, prop_pairs)?;
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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
let prop_key_id = self
.db
.propkey_id(prop_name)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown property: {prop_name}").into()))?;
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<KiteUpdateEdgeBuilder<'_>> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
Ok(KiteUpdateEdgeBuilder {
ray: self,
src,
etype_id,
dst,
updates: HashMap::new(),
})
}
pub fn upsert_edge(
&mut self,
src: NodeId,
edge_type: &str,
dst: NodeId,
) -> Result<KiteUpsertEdgeBuilder<'_>> {
let edge_def = self
.edges
.get(edge_type)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def.etype_id.ok_or_else(|| {
KiteError::InvalidSchema(format!("Edge type not initialized: {edge_type}").into())
})?;
Ok(KiteUpsertEdgeBuilder {
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(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?;
let prefix = &node_def.key_prefix;
let mut count = 0u64;
for node_id in list_nodes(&self.db) {
if let Some(key) = self.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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
Ok(count_edges(&self.db, Some(etype_id)))
}
#[cfg(feature = "bench-profile")]
pub fn take_profile_snapshot(&self) -> (u64, u64) {
self.db.take_profile_snapshot()
}
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(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?
.clone();
let prefix = node_def.key_prefix.clone();
let node_type_arc: Arc<str> = node_type.to_string().into();
Ok(list_nodes(&self.db).into_iter().filter_map(move |node_id| {
let key = self.node_key_internal(node_id)?;
if key.starts_with(&prefix) {
Some(NodeRef::new(node_id, Some(key), Arc::clone(&node_type_arc)))
} 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(|| KiteError::InvalidSchema(format!("Unknown edge type: {name}").into()))?;
edge_def.etype_id
}
None => None,
};
let options = ListEdgesOptions { etype: etype_id };
Ok(list_edges(&self.db, options).into_iter())
}
pub fn node_ref(&self, node_type: &str, key_suffix: &str) -> Result<Option<NodeRef>> {
let node_def = self
.nodes
.get(node_type)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?;
let full_key = node_def.key(key_suffix);
let node_id = 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 node_key_internal(&self, node_id: NodeId) -> Option<String> {
self.db.node_key(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) -> KiteTraversalBuilder<'_> {
KiteTraversalBuilder::new(self, vec![node_id])
}
pub fn from_nodes(&self, node_ids: Vec<NodeId>) -> KiteTraversalBuilder<'_> {
KiteTraversalBuilder::new(self, node_ids)
}
pub fn shortest_path(&self, source: NodeId, target: NodeId) -> KitePathBuilder<'_> {
KitePathBuilder::new(self, source, target)
}
pub fn shortest_path_to_any(&self, source: NodeId, targets: Vec<NodeId>) -> KitePathBuilder<'_> {
KitePathBuilder::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(|| KiteError::InvalidSchema(format!("Unknown edge type: {name}").into()))?;
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.neighbors(node_id, dir, etype_filter));
Ok(results)
}
fn neighbors(
&self,
node_id: NodeId,
direction: super::traversal::TraversalDirection,
etype: Option<ETypeId>,
) -> Vec<Edge> {
use super::traversal::TraversalDirection;
let mut edges = Vec::new();
match direction {
TraversalDirection::Out => {
for (edge_etype, dst) in self.db.out_edges(node_id) {
if etype.is_some() && etype != Some(edge_etype) {
continue;
}
edges.push(Edge {
src: node_id,
etype: edge_etype,
dst,
});
}
}
TraversalDirection::In => {
for (edge_etype, src) in self.db.in_edges(node_id) {
if etype.is_some() && etype != Some(edge_etype) {
continue;
}
edges.push(Edge {
src,
etype: edge_etype,
dst: node_id,
});
}
}
TraversalDirection::Both => {
edges.extend(self.neighbors(node_id, TraversalDirection::Out, etype));
edges.extend(self.neighbors(node_id, TraversalDirection::In, etype));
}
}
edges
}
pub fn optimize(&mut self) -> Result<()> {
self.db.optimize_single_file(None)
}
pub fn stats(&self) -> DbStats {
self.db.stats()
}
pub fn describe(&self) -> String {
let stats = self.stats();
let path = self.db.path.display();
let format = "single-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 = self.db.check();
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) -> &SingleFileDB {
&self.db
}
pub fn raw_mut(&mut self) -> &mut SingleFileDB {
&mut self.db
}
pub fn close(self) -> Result<()> {
close_single_file(self.db)
}
}
use super::traversal::{TraversalBuilder, TraversalDirection, TraversalResult, TraverseOptions};
pub struct KiteTraversalBuilder<'a> {
ray: &'a Kite,
builder: TraversalBuilder,
}
impl<'a> KiteTraversalBuilder<'a> {
fn new(ray: &'a Kite, 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.neighbors(node_id, dir, etype))
}
pub fn first(self) -> Option<TraversalResult> {
self
.builder
.first(|node_id, dir, etype| self.ray.neighbors(node_id, dir, etype))
}
pub fn first_node(self) -> Option<NodeId> {
self
.builder
.first_node(|node_id, dir, etype| self.ray.neighbors(node_id, dir, etype))
}
pub fn count(self) -> usize {
self
.builder
.count(|node_id, dir, etype| self.ray.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.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.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.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(|| KiteError::InvalidSchema(format!("Unknown edge type: {name}").into()))?;
Ok(edge_def.etype_id)
}
None => Ok(None),
}
}
}
use super::pathfinding::{bfs, dijkstra, yen_k_shortest, PathConfig, PathResult};
pub struct KitePathBuilder<'a> {
ray: &'a Kite,
source: NodeId,
targets: HashSet<NodeId>,
allowed_etypes: HashSet<ETypeId>,
direction: TraversalDirection,
max_depth: usize,
weights: HashMap<(NodeId, ETypeId, NodeId), f64>,
}
impl<'a> KitePathBuilder<'a> {
fn new(ray: &'a Kite, 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 Kite, 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(|| {
KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into())
})?;
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.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.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.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,
},
LinkWithProps {
src: NodeId,
edge_type: String,
dst: NodeId,
props: HashMap<String, PropValue>,
},
Unlink {
src: NodeId,
edge_type: String,
dst: NodeId,
},
SetProp {
node_id: NodeId,
prop_name: String,
value: PropValue,
},
SetEdgeProp {
src: NodeId,
edge_type: String,
dst: NodeId,
prop_name: String,
value: PropValue,
},
SetEdgeProps {
src: NodeId,
edge_type: String,
dst: NodeId,
props: HashMap<String, PropValue>,
},
DelProp { node_id: NodeId, prop_name: String },
}
#[derive(Debug, Clone)]
pub enum BatchResult {
NodeCreated(NodeRef),
NodeDeleted(bool),
EdgeCreated,
EdgeRemoved(bool),
PropSet,
PropDeleted,
}
#[derive(Debug, Clone)]
struct EdgeCacheEntry {
etype_id: ETypeId,
prop_key_ids: HashMap<String, PropKeyId>,
}
fn resolve_edge_cache_entry<'a>(
edge_cache: &'a mut HashMap<String, EdgeCacheEntry>,
edges: &HashMap<String, EdgeDef>,
edge_type: &str,
) -> Result<&'a mut EdgeCacheEntry> {
if edge_cache.contains_key(edge_type) {
return Ok(edge_cache.get_mut(edge_type).expect("expected value"));
}
let edge_def = edges
.get(edge_type)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
edge_cache.insert(
edge_type.to_string(),
EdgeCacheEntry {
etype_id,
prop_key_ids: edge_def.prop_key_ids.clone(),
},
);
Ok(edge_cache.get_mut(edge_type).expect("expected value"))
}
impl Kite {
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());
let mut edge_cache: HashMap<String, EdgeCacheEntry> = HashMap::new();
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(|| {
KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into())
})?;
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 entry = resolve_edge_cache_entry(&mut edge_cache, &self.edges, &edge_type)?;
let etype_id = entry.etype_id;
add_edge(&mut handle, src, etype_id, dst)?;
BatchResult::EdgeCreated
}
BatchOp::LinkWithProps {
src,
edge_type,
dst,
props,
} => {
let entry = resolve_edge_cache_entry(&mut edge_cache, &self.edges, &edge_type)?;
let etype_id = entry.etype_id;
if props.is_empty() {
add_edge(&mut handle, src, etype_id, dst)?;
} else {
let mut prop_pairs = Vec::with_capacity(props.len());
for (prop_name, value) in props {
let prop_key_id = if let Some(&id) = entry.prop_key_ids.get(&prop_name) {
id
} else {
let key_id = handle.db.propkey_id_or_create(&prop_name);
entry.prop_key_ids.insert(prop_name.clone(), key_id);
key_id
};
prop_pairs.push((prop_key_id, value));
}
handle
.db
.add_edge_with_props(src, etype_id, dst, prop_pairs)?;
}
BatchResult::EdgeCreated
}
BatchOp::Unlink {
src,
edge_type,
dst,
} => {
let edge_def = self.edges.get(&edge_type).ok_or_else(|| {
KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into())
})?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
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.propkey_id_or_create(&prop_name);
set_node_prop(&mut handle, node_id, prop_key_id, value)?;
BatchResult::PropSet
}
BatchOp::SetEdgeProp {
src,
edge_type,
dst,
prop_name,
value,
} => {
let entry = resolve_edge_cache_entry(&mut edge_cache, &self.edges, &edge_type)?;
let etype_id = entry.etype_id;
let prop_key_id = if let Some(&id) = entry.prop_key_ids.get(&prop_name) {
id
} else {
let key_id = handle.db.propkey_id_or_create(&prop_name);
entry.prop_key_ids.insert(prop_name.clone(), key_id);
key_id
};
set_edge_prop(&mut handle, src, etype_id, dst, prop_key_id, value)?;
BatchResult::PropSet
}
BatchOp::SetEdgeProps {
src,
edge_type,
dst,
props,
} => {
let entry = resolve_edge_cache_entry(&mut edge_cache, &self.edges, &edge_type)?;
let etype_id = entry.etype_id;
let mut prop_pairs = Vec::with_capacity(props.len());
for (prop_name, value) in props {
let prop_key_id = if let Some(&id) = entry.prop_key_ids.get(&prop_name) {
id
} else {
let key_id = handle.db.propkey_id_or_create(&prop_name);
entry.prop_key_ids.insert(prop_name.clone(), key_id);
key_id
};
prop_pairs.push((prop_key_id, value));
}
handle.db.set_edge_props(src, etype_id, dst, prop_pairs)?;
BatchResult::PropSet
}
BatchOp::DelProp { node_id, prop_name } => {
let prop_key_id = handle.db.propkey_id(&prop_name).ok_or_else(|| {
KiteError::InvalidSchema(format!("Unknown property: {prop_name}").into())
})?;
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(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?
.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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
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.propkey_id_or_create(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
.propkey_id(prop_name)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown property: {prop_name}").into()))?;
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(|| KiteError::InvalidSchema(format!("Unknown edge type: {edge_type}").into()))?;
let etype_id = edge_def
.etype_id
.ok_or_else(|| KiteError::InvalidSchema("Edge type not initialized".into()))?;
Ok(edge_exists(&self.handle, src, etype_id, dst))
}
pub fn prop(&self, node_id: NodeId, prop_name: &str) -> Result<Option<PropValue>> {
let prop_key_id = self
.handle
.db
.propkey_id(prop_name)
.ok_or_else(|| KiteError::InvalidSchema(format!("Unknown property: {prop_name}").into()))?;
Ok(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(|| KiteError::InvalidSchema(format!("Unknown node type: {node_type}").into()))?;
let full_key = node_def.key(key_suffix);
let node_id = 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 Kite {
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 link_with_props(
mut self,
src: NodeId,
edge_type: impl Into<String>,
dst: NodeId,
props: HashMap<String, PropValue>,
) -> Self {
self.ops.push(BatchOp::LinkWithProps {
src,
edge_type: edge_type.into(),
dst,
props,
});
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 set_edge_prop(
mut self,
src: NodeId,
edge_type: impl Into<String>,
dst: NodeId,
prop_name: impl Into<String>,
value: PropValue,
) -> Self {
self.ops.push(BatchOp::SetEdgeProp {
src,
edge_type: edge_type.into(),
dst,
prop_name: prop_name.into(),
value,
});
self
}
pub fn set_edge_props(
mut self,
src: NodeId,
edge_type: impl Into<String>,
dst: NodeId,
props: HashMap<String, PropValue>,
) -> Self {
self.ops.push(BatchOp::SetEdgeProps {
src,
edge_type: edge_type.into(),
dst,
props,
});
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 Kite) -> Result<Vec<BatchResult>> {
ray.batch(self.ops)
}
pub fn into_ops(self) -> Vec<BatchOp> {
self.ops
}
}
pub struct KiteUpdateNodeBuilder<'a> {
ray: &'a mut Kite,
node_id: NodeId,
updates: HashMap<String, Option<PropValue>>,
}
impl<'a> KiteUpdateNodeBuilder<'a> {
pub fn set(mut self, prop_name: impl Into<String>, value: PropValue) -> Self {
self.updates.insert(prop_name.into(), Some(value));
self
}
pub fn unset(mut self, prop_name: impl Into<String>) -> Self {
self.updates.insert(prop_name.into(), 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.propkey_id_or_create(&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 KiteUpsertByIdBuilder<'a> {
ray: &'a mut Kite,
node_id: NodeId,
node_def: NodeDef,
updates: HashMap<String, Option<PropValue>>,
}
impl<'a> KiteUpsertByIdBuilder<'a> {
pub fn set(mut self, prop_name: impl Into<String>, value: PropValue) -> Self {
self.updates.insert(prop_name.into(), Some(value));
self
}
pub fn unset(mut self, prop_name: impl Into<String>) -> Self {
self.updates.insert(prop_name.into(), 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<()> {
let mut handle = begin_tx(&self.ray.db)?;
let mut updates = Vec::with_capacity(self.updates.len());
for (prop_name, value_opt) in self.updates {
let prop_key_id = if let Some(&id) = self.node_def.prop_key_ids.get(&prop_name) {
id
} else {
self.ray.db.propkey_id_or_create(&prop_name)
};
updates.push((prop_key_id, value_opt));
}
let opts = NodeOpts {
key: None,
labels: self.node_def.label_id.map(|id| vec![id]),
props: None,
};
upsert_node_by_id_with_props(&mut handle, self.node_id, opts, updates)?;
commit(&mut handle)?;
Ok(())
}
}
pub struct KiteInsertBuilder<'a> {
ray: &'a mut Kite,
node_type: String,
key_prefix: String,
}
impl<'a> KiteInsertBuilder<'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 fn values_many_owned(
self,
items: Vec<(String, 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 Kite,
node_type: String,
full_key: String,
props: HashMap<String, PropValue>,
}
impl<'a> InsertExecutorSingle<'a> {
pub fn returning(self) -> Result<NodeRef> {
let node_type: Arc<str> = self.node_type.into();
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.propkey_id_or_create(&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), node_type))
}
pub fn execute(self) -> Result<()> {
let _ = self.returning()?;
Ok(())
}
}
pub struct InsertExecutorMultiple<'a> {
ray: &'a mut Kite,
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());
let node_type: Arc<str> = self.node_type.into();
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.propkey_id_or_create(&prop_name);
set_node_prop(&mut handle, node_id, prop_key_id, value)?;
}
results.push(NodeRef::new(
node_id,
Some(full_key),
Arc::clone(&node_type),
));
}
commit(&mut handle)?;
Ok(results)
}
pub fn execute(self) -> Result<()> {
let _ = self.returning()?;
Ok(())
}
}
pub struct KiteUpsertBuilder<'a> {
ray: &'a mut Kite,
node_type: String,
key_prefix: String,
}
impl<'a> KiteUpsertBuilder<'a> {
pub fn values(
self,
key_suffix: &str,
props: HashMap<String, PropValue>,
) -> Result<UpsertExecutorSingle<'a>> {
let full_key = format!("{}{}", self.key_prefix, key_suffix);
Ok(UpsertExecutorSingle {
ray: self.ray,
node_type: self.node_type,
full_key,
props,
})
}
pub fn values_many(
self,
items: Vec<(&str, HashMap<String, PropValue>)>,
) -> Result<UpsertExecutorMultiple<'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(UpsertExecutorMultiple {
ray: self.ray,
node_type: self.node_type,
entries,
})
}
pub fn values_many_owned(
self,
items: Vec<(String, HashMap<String, PropValue>)>,
) -> Result<UpsertExecutorMultiple<'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(UpsertExecutorMultiple {
ray: self.ray,
node_type: self.node_type,
entries,
})
}
}
pub struct UpsertExecutorSingle<'a> {
ray: &'a mut Kite,
node_type: String,
full_key: String,
props: HashMap<String, PropValue>,
}
impl<'a> UpsertExecutorSingle<'a> {
pub fn returning(self) -> Result<NodeRef> {
let node_type: Arc<str> = self.node_type.into();
let mut handle = begin_tx(&self.ray.db)?;
let mut updates = Vec::with_capacity(self.props.len());
for (prop_name, value) in self.props {
let prop_key_id = self.ray.db.propkey_id_or_create(&prop_name);
let value_opt = match value {
PropValue::Null => None,
other => Some(other),
};
updates.push((prop_key_id, value_opt));
}
let (node_id, _) = upsert_node_with_props(&mut handle, &self.full_key, updates)?;
commit(&mut handle)?;
Ok(NodeRef::new(node_id, Some(self.full_key), node_type))
}
pub fn execute(self) -> Result<()> {
let _ = self.returning()?;
Ok(())
}
}
pub struct UpsertExecutorMultiple<'a> {
ray: &'a mut Kite,
node_type: String,
entries: Vec<(String, HashMap<String, PropValue>)>,
}
impl<'a> UpsertExecutorMultiple<'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());
let node_type: Arc<str> = self.node_type.into();
for (full_key, props) in self.entries {
let mut updates = Vec::with_capacity(props.len());
for (prop_name, value) in props {
let prop_key_id = self.ray.db.propkey_id_or_create(&prop_name);
let value_opt = match value {
PropValue::Null => None,
other => Some(other),
};
updates.push((prop_key_id, value_opt));
}
let (node_id, _) = upsert_node_with_props(&mut handle, &full_key, updates)?;
results.push(NodeRef::new(
node_id,
Some(full_key),
Arc::clone(&node_type),
));
}
commit(&mut handle)?;
Ok(results)
}
pub fn execute(self) -> Result<()> {
let _ = self.returning()?;
Ok(())
}
}
pub struct KiteUpdateEdgeBuilder<'a> {
ray: &'a mut Kite,
src: NodeId,
etype_id: ETypeId,
dst: NodeId,
updates: HashMap<String, Option<PropValue>>,
}
impl<'a> KiteUpdateEdgeBuilder<'a> {
pub fn set(mut self, prop_name: impl Into<String>, value: PropValue) -> Self {
self.updates.insert(prop_name.into(), Some(value));
self
}
pub fn unset(mut self, prop_name: impl Into<String>) -> Self {
self.updates.insert(prop_name.into(), 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.propkey_id_or_create(&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.propkey_id(&prop_name) {
del_edge_prop(
&mut handle,
self.src,
self.etype_id,
self.dst,
existing_key_id,
)?;
}
}
}
}
commit(&mut handle)?;
Ok(())
}
}
pub struct KiteUpsertEdgeBuilder<'a> {
ray: &'a mut Kite,
src: NodeId,
etype_id: ETypeId,
dst: NodeId,
updates: HashMap<String, Option<PropValue>>,
}
impl<'a> KiteUpsertEdgeBuilder<'a> {
pub fn set(mut self, prop_name: impl Into<String>, value: PropValue) -> Self {
self.updates.insert(prop_name.into(), Some(value));
self
}
pub fn unset(mut self, prop_name: impl Into<String>) -> Self {
self.updates.insert(prop_name.into(), 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<()> {
let mut handle = begin_tx(&self.ray.db)?;
let mut updates = Vec::with_capacity(self.updates.len());
for (prop_name, value_opt) in self.updates {
let prop_key_id = self.ray.db.propkey_id_or_create(&prop_name);
updates.push((prop_key_id, value_opt));
}
upsert_edge_with_props(&mut handle, self.src, self.etype_id, self.dst, updates)?;
commit(&mut handle)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn temp_db_path(temp_dir: &tempfile::TempDir) -> std::path::PathBuf {
temp_dir.path().join("test-db")
}
fn create_test_schema() -> KiteOptions {
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");
KiteOptions::new()
.node(user)
.node(post)
.edge(follows)
.edge(authored)
}
#[test]
fn test_open_database() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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().expect("expected value");
}
#[test]
fn test_create_and_find_node() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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)
.expect("expected value");
assert!(user_ref.id > 0);
assert_eq!(user_ref.key, Some("user:alice".to_string()));
let found = ray.get("User", "alice").expect("expected value");
assert!(found.is_some());
assert_eq!(found.expect("expected value").id, user_ref.id);
let not_found = ray.get("User", "bob").expect("expected value");
assert!(not_found.is_none());
ray.close().expect("expected value");
}
#[test]
fn test_link_and_unlink() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
assert!(ray
.has_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value"));
assert!(!ray
.has_edge(bob.id, "FOLLOWS", alice.id)
.expect("expected value"));
let alice_follows = ray
.neighbors_out(alice.id, Some("FOLLOWS"))
.expect("expected value");
assert_eq!(alice_follows, vec![bob.id]);
let bob_followers = ray
.neighbors_in(bob.id, Some("FOLLOWS"))
.expect("expected value");
assert_eq!(bob_followers, vec![alice.id]);
ray
.unlink(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
assert!(!ray
.has_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value"));
ray.close().expect("expected value");
}
#[test]
fn test_properties() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Alice".to_string()));
let user = ray
.create_node("User", "alice", props)
.expect("expected value");
let name = ray.prop(user.id, "name");
assert_eq!(name, Some(PropValue::String("Alice".to_string())));
ray
.set_prop(user.id, "age", PropValue::I64(25))
.expect("expected value");
let age = ray.prop(user.id, "age");
assert_eq!(age, Some(PropValue::I64(25)));
ray.close().expect("expected value");
}
#[test]
fn test_count_nodes() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
assert_eq!(ray.count_nodes(), 0);
ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.create_node("Post", "post1", HashMap::new())
.expect("expected value");
assert_eq!(ray.count_nodes(), 3);
ray.close().expect("expected value");
}
#[test]
fn test_delete_node() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let user = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
assert!(ray.exists(user.id));
ray.delete_node(user.id).expect("expected value");
assert!(!ray.exists(user.id));
ray.close().expect("expected value");
}
#[test]
fn test_ref() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let user = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let node_ref = ray.node_ref("User", "alice").expect("expected value");
assert!(node_ref.is_some());
let node_ref = node_ref.expect("expected value");
assert_eq!(node_ref.id(), user.id);
assert_eq!(node_ref.key(), Some("user:alice"));
let not_found = ray.node_ref("User", "bob").expect("expected value");
assert!(not_found.is_none());
ray.close().expect("expected value");
}
#[test]
fn test_all_nodes_by_type() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.create_node("Post", "post1", HashMap::new())
.expect("expected value");
let users: Vec<_> = ray.all("User").expect("expected value").collect();
assert_eq!(users.len(), 2);
let posts: Vec<_> = ray.all("Post").expect("expected value").collect();
assert_eq!(posts.len(), 1);
ray.close().expect("expected value");
}
#[test]
fn test_count_nodes_by_type() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.create_node("Post", "post1", HashMap::new())
.expect("expected value");
assert_eq!(ray.count_nodes_by_type("User").expect("expected value"), 2);
assert_eq!(ray.count_nodes_by_type("Post").expect("expected value"), 1);
assert_eq!(ray.count_nodes(), 3);
ray.close().expect("expected value");
}
#[test]
fn test_all_edges() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let post = ray
.create_node("Post", "post1", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.link(alice.id, "AUTHORED", post.id)
.expect("expected value");
let all_edges: Vec<_> = ray.all_edges(None).expect("expected value").collect();
assert_eq!(all_edges.len(), 2);
let follows_edges: Vec<_> = ray
.all_edges(Some("FOLLOWS"))
.expect("expected value")
.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().expect("expected value");
}
#[test]
fn test_count_edges_by_type() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let post = ray
.create_node("Post", "post1", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.link(alice.id, "AUTHORED", post.id)
.expect("expected value");
assert_eq!(
ray.count_edges_by_type("FOLLOWS").expect("expected value"),
1
);
assert_eq!(
ray.count_edges_by_type("AUTHORED").expect("expected value"),
1
);
assert_eq!(ray.count_edges(), 2);
ray.close().expect("expected value");
}
#[test]
fn test_from_traversal() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let charlie = ray
.create_node("User", "charlie", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.link(bob.id, "FOLLOWS", charlie.id)
.expect("expected value");
let friends = ray
.from(alice.id)
.out(Some("FOLLOWS"))
.expect("expected value")
.to_vec();
assert_eq!(friends, vec![bob.id]);
let friends_of_friends = ray
.from(alice.id)
.out(Some("FOLLOWS"))
.expect("expected value")
.out(Some("FOLLOWS"))
.expect("expected value")
.to_vec();
assert_eq!(friends_of_friends, vec![charlie.id]);
ray.close().expect("expected value");
}
#[test]
fn test_traversal_first() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
let first = ray
.from(alice.id)
.out(Some("FOLLOWS"))
.expect("expected value")
.first_node();
assert_eq!(first, Some(bob.id));
let no_result = ray
.from(bob.id)
.out(Some("FOLLOWS"))
.expect("expected value")
.first_node();
assert_eq!(no_result, None);
ray.close().expect("expected value");
}
#[test]
fn test_traversal_count() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let charlie = ray
.create_node("User", "charlie", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", charlie.id)
.expect("expected value");
let count = ray
.from(alice.id)
.out(Some("FOLLOWS"))
.expect("expected value")
.count();
assert_eq!(count, 2);
ray.close().expect("expected value");
}
#[test]
fn test_shortest_path() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let charlie = ray
.create_node("User", "charlie", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.link(bob.id, "FOLLOWS", charlie.id)
.expect("expected value");
let path = ray
.shortest_path(alice.id, charlie.id)
.via("FOLLOWS")
.expect("expected value")
.find();
assert!(path.found);
assert_eq!(path.path, vec![alice.id, bob.id, charlie.id]);
assert_eq!(path.edges.len(), 2);
ray.close().expect("expected value");
}
#[test]
fn test_shortest_path_not_found() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let path = ray
.shortest_path(alice.id, bob.id)
.via("FOLLOWS")
.expect("expected value")
.find();
assert!(!path.found);
assert!(path.path.is_empty());
ray.close().expect("expected value");
}
#[test]
fn test_has_path() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let charlie = ray
.create_node("User", "charlie", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
assert!(ray
.has_path(alice.id, bob.id, Some("FOLLOWS"))
.expect("expected value"));
assert!(!ray
.has_path(alice.id, charlie.id, Some("FOLLOWS"))
.expect("expected value"));
assert!(!ray
.has_path(bob.id, alice.id, Some("FOLLOWS"))
.expect("expected value"));
ray.close().expect("expected value");
}
#[test]
fn test_reachable_from() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let charlie = ray
.create_node("User", "charlie", HashMap::new())
.expect("expected value");
let dave = ray
.create_node("User", "dave", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.link(bob.id, "FOLLOWS", charlie.id)
.expect("expected value");
ray
.link(charlie.id, "FOLLOWS", dave.id)
.expect("expected value");
let reachable = ray
.reachable_from(alice.id, 2, Some("FOLLOWS"))
.expect("expected value");
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"))
.expect("expected value");
assert!(reachable_3.contains(&dave.id));
ray.close().expect("expected value");
}
#[test]
fn test_k_shortest_paths() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let charlie = ray
.create_node("User", "charlie", HashMap::new())
.expect("expected value");
let dave = ray
.create_node("User", "dave", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", charlie.id)
.expect("expected value");
ray
.link(bob.id, "FOLLOWS", dave.id)
.expect("expected value");
ray
.link(charlie.id, "FOLLOWS", dave.id)
.expect("expected value");
let paths = ray
.shortest_path(alice.id, dave.id)
.via("FOLLOWS")
.expect("expected value")
.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().expect("expected value");
}
#[test]
fn test_batch_create_nodes() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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(),
},
])
.expect("expected value");
assert_eq!(results.len(), 3);
assert_eq!(ray.count_nodes(), 3);
assert!(ray.get("User", "alice").expect("expected value").is_some());
assert!(ray.get("User", "bob").expect("expected value").is_some());
assert!(ray.get("Post", "post1").expect("expected value").is_some());
ray.close().expect("expected value");
}
#[test]
fn test_batch_create_and_link() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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(),
},
])
.expect("expected value");
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,
}])
.expect("expected value");
assert!(ray
.has_edge(alice_id, "FOLLOWS", bob_id)
.expect("expected value"));
ray.close().expect("expected value");
}
#[test]
fn test_batch_link_with_props() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let mut props = HashMap::new();
props.insert("weight".to_string(), PropValue::F64(0.9));
props.insert("since".to_string(), PropValue::String("2025".into()));
ray
.batch(vec![BatchOp::LinkWithProps {
src: alice.id,
edge_type: "FOLLOWS".into(),
dst: bob.id,
props,
}])
.expect("expected value");
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, Some(PropValue::F64(0.9)));
let since = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.expect("expected value");
assert_eq!(since, Some(PropValue::String("2025".into())));
ray.close().expect("expected value");
}
#[test]
fn test_batch_set_properties() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let user = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
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),
},
])
.expect("expected value");
assert_eq!(
ray.prop(user.id, "name"),
Some(PropValue::String("Alice".into()))
);
assert_eq!(ray.prop(user.id, "age"), Some(PropValue::I64(30)));
ray.close().expect("expected value");
}
#[test]
fn test_batch_set_edge_properties() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.batch(vec![
BatchOp::SetEdgeProp {
src: alice.id,
edge_type: "FOLLOWS".into(),
dst: bob.id,
prop_name: "weight".into(),
value: PropValue::F64(0.75),
},
BatchOp::SetEdgeProp {
src: alice.id,
edge_type: "FOLLOWS".into(),
dst: bob.id,
prop_name: "since".into(),
value: PropValue::String("2024".into()),
},
])
.expect("expected value");
assert_eq!(
ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value"),
Some(PropValue::F64(0.75))
);
assert_eq!(
ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.expect("expected value"),
Some(PropValue::String("2024".into()))
);
ray.close().expect("expected value");
}
#[test]
fn test_batch_set_edge_props() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
let mut props = HashMap::new();
props.insert("weight".to_string(), PropValue::F64(0.33));
props.insert("since".to_string(), PropValue::String("2023".into()));
ray
.batch(vec![BatchOp::SetEdgeProps {
src: alice.id,
edge_type: "FOLLOWS".into(),
dst: bob.id,
props,
}])
.expect("expected value");
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, Some(PropValue::F64(0.33)));
let since = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.expect("expected value");
assert_eq!(since, Some(PropValue::String("2023".into())));
ray.close().expect("expected value");
}
#[test]
fn test_batch_mixed_operations() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
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,
},
])
.expect("expected value");
assert_eq!(results.len(), 3);
assert_eq!(
ray.prop(alice.id, "name"),
Some(PropValue::String("Alice".into()))
);
assert!(ray
.get("User", "charlie")
.expect("expected value")
.is_some());
assert!(!ray
.has_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value"));
ray.close().expect("expected value");
}
#[test]
fn test_batch_delete_operations() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
let results = ray
.batch(vec![
BatchOp::Unlink {
src: alice.id,
edge_type: "FOLLOWS".into(),
dst: bob.id,
},
BatchOp::DeleteNode { node_id: bob.id },
])
.expect("expected value");
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().expect("expected value");
}
#[test]
fn test_transaction_basic() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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))
})
.expect("expected value");
assert!(ray.exists(alice.id));
assert!(ray.exists(bob.id));
assert!(ray
.has_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value"));
ray.close().expect("expected value");
}
#[test]
fn test_transaction_with_properties() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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)
})
.expect("expected value");
assert_eq!(
ray.prop(alice.id, "name"),
Some(PropValue::String("Alice".into()))
);
assert_eq!(ray.prop(alice.id, "age"), Some(PropValue::I64(30)));
ray.close().expect("expected value");
}
#[test]
fn test_transaction_rollback_on_error() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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().expect("expected value");
}
#[test]
fn test_transaction_read_operations() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
ray
.set_prop(alice.id, "name", PropValue::String("Alice".into()))
.expect("expected value");
let name = ray
.transaction(|ctx| {
let existing = ctx.get("User", "alice")?;
assert!(existing.is_some());
let name = ctx.prop(alice.id, "name")?;
assert!(ctx.exists(alice.id));
ctx.create_node("User", "bob", HashMap::new())?;
Ok(name)
})
.expect("expected value");
assert_eq!(name, Some(PropValue::String("Alice".into())));
assert!(ray.get("User", "bob").expect("expected value").is_some());
ray.close().expect("expected value");
}
#[test]
fn test_transaction_edge_operations() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let charlie = ray
.create_node("User", "charlie", HashMap::new())
.expect("expected value");
ray
.transaction(|ctx| {
ctx.link(alice.id, "FOLLOWS", bob.id)?;
ctx.link(bob.id, "FOLLOWS", charlie.id)?;
Ok(())
})
.expect("expected value");
assert!(ray
.has_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value"));
assert!(ray
.has_edge(bob.id, "FOLLOWS", charlie.id)
.expect("expected value"));
assert!(!ray
.has_edge(alice.id, "FOLLOWS", charlie.id)
.expect("expected value"));
ray
.transaction(|ctx| {
ctx.unlink(alice.id, "FOLLOWS", bob.id)?;
Ok(())
})
.expect("expected value");
assert!(!ray
.has_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value"));
assert!(ray
.has_edge(bob.id, "FOLLOWS", charlie.id)
.expect("expected value"));
ray.close().expect("expected value");
}
#[test]
fn test_tx_builder() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let results = ray
.tx()
.create_node("User", "alice", HashMap::new())
.create_node("User", "bob", HashMap::new())
.execute(&mut ray)
.expect("expected value");
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)
.expect("expected value");
assert!(ray
.has_edge(alice_id, "FOLLOWS", bob_id)
.expect("expected value"));
assert_eq!(
ray.prop(alice_id, "name"),
Some(PropValue::String("Alice".into()))
);
ray.close().expect("expected value");
}
#[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().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
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)
.expect("expected value");
assert!(ray
.has_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value"));
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, Some(PropValue::F64(0.8)));
let since = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.expect("expected value");
assert_eq!(since, Some(PropValue::String("2024".into())));
ray.close().expect("expected value");
}
#[test]
fn test_set_edge_prop() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.set_edge_prop(alice.id, "FOLLOWS", bob.id, "weight", PropValue::F64(0.5))
.expect("expected value");
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, Some(PropValue::F64(0.5)));
ray
.set_edge_prop(alice.id, "FOLLOWS", bob.id, "weight", PropValue::F64(0.9))
.expect("expected value");
let new_weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(new_weight, Some(PropValue::F64(0.9)));
ray.close().expect("expected value");
}
#[test]
fn test_set_edge_props() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
let mut props = HashMap::new();
props.insert("weight".to_string(), PropValue::F64(0.6));
props.insert("since".to_string(), PropValue::String("2024".into()));
ray
.set_edge_props(alice.id, "FOLLOWS", bob.id, props)
.expect("expected value");
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, Some(PropValue::F64(0.6)));
let since = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.expect("expected value");
assert_eq!(since, Some(PropValue::String("2024".into())));
ray.close().expect("expected value");
}
#[test]
fn test_edge_props() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
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)
.expect("expected value");
let all_props = ray
.edge_props(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
assert!(all_props.is_some());
let all_props = all_props.expect("expected value");
assert_eq!(all_props.get("weight"), Some(&PropValue::F64(0.7)));
assert_eq!(
all_props.get("type"),
Some(&PropValue::String("friend".into()))
);
ray.close().expect("expected value");
}
#[test]
fn test_del_edge_prop() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.set_edge_prop(alice.id, "FOLLOWS", bob.id, "weight", PropValue::F64(0.5))
.expect("expected value");
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, Some(PropValue::F64(0.5)));
ray
.del_edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, None);
ray.close().expect("expected value");
}
#[test]
fn test_edge_prop_nonexistent_edge() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.set_edge_prop(alice.id, "FOLLOWS", bob.id, "weight", PropValue::F64(0.5))
.ok();
let _props = ray
.edge_props(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
let charlie = ray
.create_node("User", "charlie", HashMap::new())
.expect("expected value");
let props2 = ray
.edge_props(alice.id, "FOLLOWS", charlie.id)
.expect("expected value");
assert!(props2.is_none());
ray.close().expect("expected value");
}
#[test]
fn test_update_edge_builder() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.update_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value")
.set("weight", PropValue::F64(0.9))
.set("since", PropValue::String("2024".into()))
.execute()
.expect("expected value");
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, Some(PropValue::F64(0.9)));
let since = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.expect("expected value");
assert_eq!(since, Some(PropValue::String("2024".into())));
ray
.update_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value")
.set("weight", PropValue::F64(0.5))
.unset("since")
.execute()
.expect("expected value");
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, Some(PropValue::F64(0.5)));
let since = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.expect("expected value");
assert_eq!(since, None);
ray.close().expect("expected value");
}
#[test]
fn test_update_edge_builder_set_all() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
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)
.expect("expected value")
.set_all(props)
.execute()
.expect("expected value");
let all_props = ray
.edge_props(alice.id, "FOLLOWS", bob.id)
.expect("expected value")
.expect("expected value");
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().expect("expected value");
}
#[test]
fn test_update_edge_builder_empty() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.update_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value")
.execute()
.expect("expected value");
ray.close().expect("expected value");
}
#[test]
fn test_upsert_edge_builder() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.upsert_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value")
.set("since", PropValue::I64(2020))
.execute()
.expect("expected value");
let since = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.expect("expected value");
assert_eq!(since, Some(PropValue::I64(2020)));
ray
.upsert_edge(alice.id, "FOLLOWS", bob.id)
.expect("expected value")
.set("weight", PropValue::F64(0.5))
.unset("since")
.execute()
.expect("expected value");
let since = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "since")
.expect("expected value");
assert_eq!(since, None);
let weight = ray
.edge_prop(alice.id, "FOLLOWS", bob.id, "weight")
.expect("expected value");
assert_eq!(weight, Some(PropValue::F64(0.5)));
ray.close().expect("expected value");
}
#[test]
fn test_insert_builder_returning() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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")
.expect("expected value")
.values("alice", props)
.expect("expected value")
.returning()
.expect("expected value");
assert!(alice.id > 0);
assert_eq!(alice.key, Some("user:alice".to_string()));
assert_eq!(alice.node_type.as_ref(), "User");
let name = ray.prop(alice.id, "name");
assert_eq!(name, Some(PropValue::String("Alice".into())));
let age = ray.prop(alice.id, "age");
assert_eq!(age, Some(PropValue::I64(30)));
ray.close().expect("expected value");
}
#[test]
fn test_insert_builder_execute() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Bob".into()));
ray
.insert("User")
.expect("expected value")
.values("bob", props)
.expect("expected value")
.execute()
.expect("expected value");
let bob = ray.get("User", "bob").expect("expected value");
assert!(bob.is_some());
let bob = bob.expect("expected value");
let name = ray.prop(bob.id, "name");
assert_eq!(name, Some(PropValue::String("Bob".into())));
ray.close().expect("expected value");
}
#[test]
fn test_insert_builder_values_many() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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")
.expect("expected value")
.values_many(vec![
("alice", alice_props),
("bob", bob_props),
("charlie", charlie_props),
])
.expect("expected value")
.returning()
.expect("expected value");
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().expect("expected value");
}
#[test]
fn test_insert_builder_empty_values_many() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let users = ray
.insert("User")
.expect("expected value")
.values_many(vec![])
.expect("expected value")
.returning()
.expect("expected value");
assert_eq!(users.len(), 0);
assert_eq!(ray.count_nodes(), 0);
ray.close().expect("expected value");
}
#[test]
fn test_check_empty_database() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let result = ray.check().expect("expected value");
assert!(result.valid);
assert!(result.errors.is_empty());
assert!(result
.warnings
.iter()
.any(|w| w.contains("No nodes in database")));
ray.close().expect("expected value");
}
#[test]
fn test_check_valid_database() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
let charlie = ray
.create_node("User", "charlie", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
ray
.link(bob.id, "FOLLOWS", charlie.id)
.expect("expected value");
ray
.link(charlie.id, "FOLLOWS", alice.id)
.expect("expected value");
let result = ray.check().expect("expected value");
assert!(
result.valid,
"Expected valid database, got errors: {:?}",
result.errors
);
assert!(result.errors.is_empty());
ray.close().expect("expected value");
}
#[test]
fn test_check_with_properties() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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)
.expect("expected value");
let mut props2 = HashMap::new();
props2.insert("name".to_string(), PropValue::String("Bob".into()));
let bob = ray
.create_node("User", "bob", props2)
.expect("expected value");
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)
.expect("expected value");
let result = ray.check().expect("expected value");
assert!(
result.valid,
"Expected valid database, got errors: {:?}",
result.errors
);
assert!(result.errors.is_empty());
ray.close().expect("expected value");
}
#[test]
fn test_update_node_by_ref() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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)
.expect("expected value");
ray
.update(&alice)
.expect("expected value")
.set("name", PropValue::String("Alice Updated".into()))
.set("age", PropValue::I64(31))
.execute()
.expect("expected value");
let name = ray.prop(alice.id, "name");
assert_eq!(name, Some(PropValue::String("Alice Updated".into())));
let age = ray.prop(alice.id, "age");
assert_eq!(age, Some(PropValue::I64(31)));
ray.close().expect("expected value");
}
#[test]
fn test_update_node_by_key() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Bob".into()));
ray
.create_node("User", "bob", props)
.expect("expected value");
ray
.update_by_key("User", "bob")
.expect("expected value")
.set("name", PropValue::String("Bob Updated".into()))
.set("age", PropValue::I64(25))
.execute()
.expect("expected value");
let bob = ray
.get("User", "bob")
.expect("expected value")
.expect("expected value");
let name = ray.prop(bob.id, "name");
assert_eq!(name, Some(PropValue::String("Bob Updated".into())));
let age = ray.prop(bob.id, "age");
assert_eq!(age, Some(PropValue::I64(25)));
ray.close().expect("expected value");
}
#[test]
fn test_update_node_by_id() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Charlie".into()));
let charlie = ray
.create_node("User", "charlie", props)
.expect("expected value");
ray
.update_by_id(charlie.id)
.expect("expected value")
.set("name", PropValue::String("Charlie Updated".into()))
.execute()
.expect("expected value");
let name = ray.prop(charlie.id, "name");
assert_eq!(name, Some(PropValue::String("Charlie Updated".into())));
ray.close().expect("expected value");
}
#[test]
fn test_upsert_node_by_id_builder() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
ray
.upsert_by_id("User", 42)
.expect("expected value")
.set("name", PropValue::String("Alice".into()))
.set("age", PropValue::I64(30))
.execute()
.expect("expected value");
assert!(ray.exists(42));
let name = ray.prop(42, "name");
assert_eq!(name, Some(PropValue::String("Alice".into())));
let age = ray.prop(42, "age");
assert_eq!(age, Some(PropValue::I64(30)));
ray
.upsert_by_id("User", 42)
.expect("expected value")
.set("age", PropValue::I64(31))
.unset("name")
.execute()
.expect("expected value");
let name = ray.prop(42, "name");
assert_eq!(name, None);
let age = ray.prop(42, "age");
assert_eq!(age, Some(PropValue::I64(31)));
ray.close().expect("expected value");
}
#[test]
fn test_update_node_unset() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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)
.expect("expected value");
assert!(ray.prop(dave.id, "age").is_some());
ray
.update(&dave)
.expect("expected value")
.set("name", PropValue::String("Dave Updated".into()))
.unset("age")
.execute()
.expect("expected value");
let name = ray.prop(dave.id, "name");
assert_eq!(name, Some(PropValue::String("Dave Updated".into())));
let age = ray.prop(dave.id, "age");
assert_eq!(age, None);
ray.close().expect("expected value");
}
#[test]
fn test_update_node_set_all() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let eve = ray
.create_node("User", "eve", HashMap::new())
.expect("expected value");
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)
.expect("expected value")
.set_all(updates)
.execute()
.expect("expected value");
let name = ray.prop(eve.id, "name");
assert_eq!(name, Some(PropValue::String("Eve".into())));
let age = ray.prop(eve.id, "age");
assert_eq!(age, Some(PropValue::I64(28)));
ray.close().expect("expected value");
}
#[test]
fn test_update_node_nonexistent() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
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().expect("expected value");
}
#[test]
fn test_update_node_empty() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let mut props = HashMap::new();
props.insert("name".to_string(), PropValue::String("Frank".into()));
let frank = ray
.create_node("User", "frank", props)
.expect("expected value");
ray
.update(&frank)
.expect("expected value")
.execute()
.expect("expected value");
let name = ray.prop(frank.id, "name");
assert_eq!(name, Some(PropValue::String("Frank".into())));
ray.close().expect("expected value");
}
#[test]
fn test_describe() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
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().expect("expected value");
}
#[test]
fn test_stats() {
let temp_dir = tempdir().expect("expected value");
let options = create_test_schema();
let mut ray = Kite::open(temp_db_path(&temp_dir), options).expect("expected value");
let alice = ray
.create_node("User", "alice", HashMap::new())
.expect("expected value");
let bob = ray
.create_node("User", "bob", HashMap::new())
.expect("expected value");
ray
.link(alice.id, "FOLLOWS", bob.id)
.expect("expected value");
let stats = ray.stats();
assert!(stats.snapshot_nodes + stats.delta_nodes_created as u64 >= 2);
assert!(stats.snapshot_edges + stats.delta_edges_added as u64 >= 1);
ray.close().expect("expected value");
}
}