pub mod backends;
#[cfg(feature = "crdt")]
pub mod crdt;
pub mod error;
pub mod index;
pub mod node;
pub mod predicate;
pub mod query;
pub mod store;
pub mod triple;
pub mod value;
#[cfg(feature = "rdf")]
pub mod rdf;
#[cfg(feature = "dag")]
pub mod dag;
pub use error::{Error, Result};
pub use index::{IndexType, TripleIndex};
pub use node::NodeId;
pub use predicate::Predicate;
pub use query::{QueryBuilder, QueryResult, TriplePattern};
pub use store::GraphStore;
pub use triple::{Triple, TripleBuilder, TripleId, TripleMeta};
pub use value::Value;
#[cfg(feature = "sled-backend")]
pub use backends::sled::SledBackend;
#[cfg(feature = "rocksdb-backend")]
pub use backends::rocksdb::RocksBackend;
#[cfg(feature = "sqlite-backend")]
pub use backends::sqlite::SqliteBackend;
pub use backends::memory::MemoryBackend;
pub struct GraphDB {
store: GraphStore,
#[cfg(feature = "dag")]
dag_store: Option<dag::DagStore>,
}
impl GraphDB {
pub fn memory() -> Result<Self> {
let backend = MemoryBackend::new();
let store = GraphStore::new(Box::new(backend))?;
Ok(Self {
store,
#[cfg(feature = "dag")]
dag_store: None,
})
}
#[cfg(feature = "sled-backend")]
pub fn sled(path: &str) -> Result<Self> {
let backend = SledBackend::open(path)?;
let store = GraphStore::new(Box::new(backend))?;
Ok(Self {
store,
#[cfg(feature = "dag")]
dag_store: None,
})
}
#[cfg(feature = "rocksdb-backend")]
pub fn rocksdb(path: &str) -> Result<Self> {
let backend = RocksBackend::open(path)?;
let store = GraphStore::new(Box::new(backend))?;
Ok(Self {
store,
#[cfg(feature = "dag")]
dag_store: None,
})
}
#[cfg(feature = "sqlite-backend")]
pub fn sqlite(path: &str) -> Result<Self> {
let backend = SqliteBackend::open(path)?;
let store = GraphStore::new(Box::new(backend))?;
Ok(Self {
store,
#[cfg(feature = "dag")]
dag_store: None,
})
}
#[cfg(feature = "dag")]
pub fn memory_with_dag() -> Result<Self> {
let backend = MemoryBackend::new();
let store = GraphStore::new(Box::new(backend))?;
Ok(Self {
store,
dag_store: Some(dag::DagStore::new()),
})
}
#[cfg(all(feature = "dag", feature = "sled-backend"))]
pub fn sled_with_dag(path: &str) -> Result<Self> {
let backend = SledBackend::open(path)?;
let dag_backend = dag::SledDagBackend::from_db(backend.db())?;
let store = GraphStore::new(Box::new(backend))?;
Ok(Self {
store,
dag_store: Some(dag::DagStore::with_backend(Box::new(dag_backend))?),
})
}
#[cfg(feature = "dag")]
pub fn enable_dag(&mut self) {
if self.dag_store.is_none() {
self.dag_store = Some(dag::DagStore::new());
}
}
#[cfg(all(feature = "dag", feature = "sled-backend"))]
pub fn enable_dag_persistent(&mut self, path: &str) -> Result<()> {
if self.dag_store.is_none() {
let dag_backend = if let Some(sled_be) = self
.store
.backend_as_any()
.downcast_ref::<crate::backends::SledBackend>()
{
dag::SledDagBackend::from_db(sled_be.db())?
} else {
dag::SledDagBackend::open(path)?
};
self.dag_store = Some(dag::DagStore::with_backend(Box::new(dag_backend))?);
}
Ok(())
}
#[cfg(feature = "dag")]
pub fn dag_store(&self) -> Option<&dag::DagStore> {
self.dag_store.as_ref()
}
#[cfg(feature = "dag")]
pub fn insert_via_dag(
&self,
triple: Triple,
author: NodeId,
seq: u64,
parents: Vec<dag::DagActionHash>,
) -> Result<(dag::DagActionHash, TripleId)> {
let triple_id = self.store.insert(triple.clone())?;
let dag_store = self
.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?;
let action = dag::DagAction {
parents,
author,
seq,
timestamp: chrono::Utc::now(),
payload: dag::DagPayload::TripleInsert {
triples: vec![dag::TripleInsertPayload {
subject: triple.subject.to_string(),
predicate: triple.predicate.to_string(),
object: value_to_json(&triple.object),
provenance: None,
}],
},
signature: None,
};
let hash = dag_store.put(&action)?;
Ok((hash, triple_id))
}
#[cfg(feature = "dag")]
pub fn delete_via_dag(
&self,
triple_id: &TripleId,
author: NodeId,
seq: u64,
parents: Vec<dag::DagActionHash>,
) -> Result<dag::DagActionHash> {
self.store.delete(triple_id)?;
let dag_store = self
.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?;
let action = dag::DagAction {
parents,
author,
seq,
timestamp: chrono::Utc::now(),
payload: dag::DagPayload::TripleDelete {
triple_ids: vec![*triple_id.as_bytes()],
subjects: vec![],
},
signature: None,
};
dag_store.put(&action)
}
#[cfg(feature = "dag")]
pub fn dag_tips(&self) -> Result<Vec<dag::DagActionHash>> {
self.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?
.tips()
}
#[cfg(feature = "dag")]
pub fn dag_action(&self, hash: &dag::DagActionHash) -> Result<Option<dag::DagAction>> {
self.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?
.get(hash)
}
#[cfg(feature = "dag")]
pub fn dag_history(&self, triple_id: &[u8; 32], limit: usize) -> Result<Vec<dag::DagAction>> {
self.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?
.history(triple_id, limit)
}
#[cfg(feature = "dag")]
pub fn dag_history_by_subject(
&self,
subject: &str,
limit: usize,
) -> Result<Vec<dag::DagAction>> {
self.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?
.history_by_subject(subject, limit)
}
#[cfg(feature = "dag")]
pub fn dag_chain(&self, author: &NodeId, limit: usize) -> Result<Vec<dag::DagAction>> {
self.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?
.chain(author, limit)
}
#[cfg(feature = "dag")]
pub fn dag_prune(
&self,
policy: &dag::RetentionPolicy,
create_checkpoint: bool,
) -> Result<dag::PruneResult> {
self.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?
.prune(policy, create_checkpoint)
}
#[cfg(feature = "dag")]
pub fn dag_at(
&self,
target: &dag::DagActionHash,
) -> Result<(GraphDB, dag::TimeTravelSnapshot)> {
let dag_store = self
.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?;
let actions = dag_store.ancestors(target)?;
let snapshot_db = GraphDB::memory()?;
for action in &actions {
dag::timetravel::replay_payload(&snapshot_db, &action.payload)?;
}
let target_action = dag_store
.get(target)?
.ok_or_else(|| Error::NotFound(format!("DagAction {} not found", target)))?;
let info = dag::TimeTravelSnapshot {
target_hash: *target,
target_timestamp: target_action.timestamp,
actions_replayed: actions.len(),
triple_count: snapshot_db.count(),
};
Ok((snapshot_db, info))
}
#[cfg(feature = "dag")]
pub fn dag_at_timestamp(
&self,
ts: &chrono::DateTime<chrono::Utc>,
) -> Result<(GraphDB, dag::TimeTravelSnapshot)> {
let dag_store = self
.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?;
let target = dag_store
.action_at_or_before(ts)?
.ok_or_else(|| Error::NotFound("No actions found before the given timestamp".into()))?;
self.dag_at(&target)
}
#[cfg(feature = "dag-sign")]
pub fn dag_sign(&self, action: &mut dag::DagAction, key: &dag::DagSigningKey) {
key.sign(action);
}
#[cfg(feature = "dag-sign")]
pub fn dag_verify(
&self,
action: &dag::DagAction,
public_key: &[u8; 32],
) -> Result<dag::VerifyResult> {
dag::signing::verify_action(action, public_key).map_err(|e| Error::Config(e.to_string()))
}
#[cfg(feature = "dag")]
pub fn dag_export(&self) -> Result<dag::DagGraph> {
self.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?
.export_graph()
}
#[cfg(feature = "dag")]
pub fn dag_ingest(&self, action: &dag::DagAction) -> Result<dag::DagActionHash> {
self.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?
.ingest(action)
}
#[cfg(feature = "dag")]
pub fn dag_compute_missing(
&self,
remote_tips: &[dag::DagActionHash],
) -> Result<Vec<dag::DagAction>> {
self.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?
.compute_missing(remote_tips)
}
#[cfg(feature = "dag")]
pub fn dag_diff(
&self,
from: &dag::DagActionHash,
to: &dag::DagActionHash,
) -> Result<dag::DagDiff> {
let dag_store = self
.dag_store
.as_ref()
.ok_or_else(|| Error::Config("DAG not enabled".into()))?;
let actions = dag_store.actions_between(from, to)?;
Ok(dag::DagDiff {
from: *from,
to: *to,
actions,
})
}
pub fn insert(&self, triple: Triple) -> Result<TripleId> {
self.store.insert(triple)
}
pub fn insert_batch(&self, triples: Vec<Triple>) -> Result<Vec<TripleId>> {
self.store.insert_batch(triples)
}
pub fn get(&self, id: &TripleId) -> Result<Option<Triple>> {
self.store.get(id)
}
pub fn delete(&self, id: &TripleId) -> Result<bool> {
self.store.delete(id)
}
pub fn query(&self) -> QueryBuilder<'_> {
QueryBuilder::new(&self.store)
}
pub fn find(&self, pattern: TriplePattern) -> Result<Vec<Triple>> {
self.store.find(pattern)
}
pub fn traverse(&self, start: &NodeId, predicates: &[Predicate]) -> Result<Vec<NodeId>> {
self.store.traverse(start, predicates)
}
pub fn stats(&self) -> GraphStats {
self.store.stats()
}
pub fn flush(&self) -> Result<()> {
self.store.flush()?;
#[cfg(feature = "dag")]
if let Some(dag) = &self.dag_store {
dag.flush()?;
}
Ok(())
}
pub fn count(&self) -> usize {
self.store.count()
}
pub fn contains(&self, triple: &Triple) -> Result<bool> {
self.store.contains(triple)
}
pub fn get_subject(&self, subject: &NodeId) -> Result<Vec<Triple>> {
self.find(TriplePattern::subject(subject.clone()))
}
pub fn get_predicate(&self, predicate: &Predicate) -> Result<Vec<Triple>> {
self.find(TriplePattern::predicate(predicate.clone()))
}
pub fn get_object(&self, object: &Value) -> Result<Vec<Triple>> {
self.find(TriplePattern::object(object.clone()))
}
pub fn subjects_with_prefix(&self, prefix: &str) -> Result<Vec<NodeId>> {
let all = self.find(TriplePattern::any())?;
let mut seen = std::collections::HashSet::new();
let mut result = Vec::new();
for triple in all {
if let Some(name) = triple.subject.as_name() {
if name.starts_with(prefix) && seen.insert(triple.subject.clone()) {
result.push(triple.subject.clone());
}
}
}
Ok(result)
}
pub fn delete_by_subject_prefix(&self, prefix: &str) -> Result<usize> {
let all = self.find(TriplePattern::any())?;
let mut deleted = 0usize;
for triple in all {
if let Some(name) = triple.subject.as_name() {
if name.starts_with(prefix) {
if self.delete(&triple.id())? {
deleted += 1;
}
}
}
}
Ok(deleted)
}
#[cfg(feature = "rdf")]
pub fn import_turtle(&self, turtle: &str) -> Result<Vec<TripleId>> {
use rdf::{RdfParser, TurtleParser};
let triples = TurtleParser::parse_to_triples(turtle)?;
self.insert_batch(triples)
}
#[cfg(feature = "rdf")]
pub fn import_ntriples(&self, ntriples: &str) -> Result<Vec<TripleId>> {
use rdf::{NTriplesParser, RdfParser};
let triples = NTriplesParser::parse_to_triples(ntriples)?;
self.insert_batch(triples)
}
#[cfg(feature = "rdf")]
pub fn export_turtle(&self) -> Result<String> {
use rdf::{RdfSerializer, TurtleSerializer};
let triples = self.find(TriplePattern::any())?;
TurtleSerializer::serialize_triples(&triples)
}
#[cfg(feature = "rdf")]
pub fn export_ntriples(&self) -> Result<String> {
use rdf::{NTriplesSerializer, RdfSerializer};
let triples = self.find(TriplePattern::any())?;
NTriplesSerializer::serialize_triples(&triples)
}
#[cfg(feature = "rdf")]
pub fn export_turtle_pattern(&self, pattern: TriplePattern) -> Result<String> {
use rdf::{RdfSerializer, TurtleSerializer};
let triples = self.find(pattern)?;
TurtleSerializer::serialize_triples(&triples)
}
}
#[derive(Debug, Clone, Default)]
pub struct GraphStats {
pub triple_count: usize,
pub subject_count: usize,
pub predicate_count: usize,
pub object_count: usize,
pub storage_bytes: usize,
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(feature = "dag")]
fn value_to_json(v: &Value) -> serde_json::Value {
match v {
Value::String(s) => serde_json::Value::String(s.clone()),
Value::Integer(i) => serde_json::json!(*i),
Value::Float(f) => serde_json::json!(*f),
Value::Boolean(b) => serde_json::json!(*b),
Value::Json(j) => j.clone(),
Value::Node(n) => serde_json::json!({ "node": n.to_string() }),
Value::DateTime(dt) => serde_json::Value::String(dt.clone()),
Value::Null => serde_json::Value::Null,
_ => serde_json::Value::String(format!("{:?}", v)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_memory_db() {
let db = GraphDB::memory().unwrap();
assert_eq!(db.count(), 0);
}
#[test]
fn test_insert_and_get() {
let db = GraphDB::memory().unwrap();
let triple = Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_name"),
Value::literal("Alice"),
);
let id = db.insert(triple.clone()).unwrap();
let retrieved = db.get(&id).unwrap().unwrap();
assert_eq!(retrieved.subject, triple.subject);
assert_eq!(retrieved.predicate, triple.predicate);
assert_eq!(retrieved.object, triple.object);
}
#[test]
fn test_count() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::new(
NodeId::named("a"),
Predicate::named("p"),
Value::literal("b"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("c"),
Predicate::named("p"),
Value::literal("d"),
))
.unwrap();
assert_eq!(db.count(), 2);
}
#[test]
fn test_insert_batch() {
let db = GraphDB::memory().unwrap();
let triples = vec![
Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_age"),
Value::integer(30),
),
Triple::new(
NodeId::named("user:bob"),
Predicate::named("has_age"),
Value::integer(25),
),
Triple::new(
NodeId::named("user:charlie"),
Predicate::named("has_age"),
Value::integer(35),
),
];
let ids = db.insert_batch(triples).unwrap();
assert_eq!(ids.len(), 3);
assert_eq!(db.count(), 3);
}
#[test]
fn test_delete() {
let db = GraphDB::memory().unwrap();
let triple = Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_name"),
Value::literal("Alice"),
);
let id = db.insert(triple).unwrap();
assert_eq!(db.count(), 1);
let deleted = db.delete(&id).unwrap();
assert!(deleted);
assert_eq!(db.count(), 0);
let deleted_again = db.delete(&id).unwrap();
assert!(!deleted_again);
}
#[test]
fn test_query_by_subject() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_name"),
Value::literal("Alice"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_age"),
Value::integer(30),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("user:bob"),
Predicate::named("has_name"),
Value::literal("Bob"),
))
.unwrap();
let results = db
.query()
.subject(NodeId::named("user:alice"))
.execute()
.unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_query_with_limit() {
let db = GraphDB::memory().unwrap();
for i in 0..10 {
db.insert(Triple::new(
NodeId::named(&format!("node:{}", i)),
Predicate::named("has_value"),
Value::integer(i as i64),
))
.unwrap();
}
let results = db.query().limit(5).execute().unwrap();
assert_eq!(results.len(), 5);
}
#[test]
fn test_find_by_pattern() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_name"),
Value::literal("Alice"),
))
.unwrap();
let pattern = TriplePattern::subject(NodeId::named("user:alice"));
let results = db.find(pattern).unwrap();
assert_eq!(results.len(), 1);
}
#[test]
fn test_traverse() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::link(
NodeId::named("alice"),
Predicate::named("knows"),
NodeId::named("bob"),
))
.unwrap();
db.insert(Triple::link(
NodeId::named("bob"),
Predicate::named("knows"),
NodeId::named("charlie"),
))
.unwrap();
let reachable = db
.traverse(&NodeId::named("alice"), &[Predicate::named("knows")])
.unwrap();
assert!(reachable.contains(&NodeId::named("bob")));
assert!(reachable.contains(&NodeId::named("charlie")));
}
#[test]
fn test_stats() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_name"),
Value::literal("Alice"),
))
.unwrap();
let stats = db.stats();
assert_eq!(stats.triple_count, 1);
}
#[test]
fn test_contains() {
let db = GraphDB::memory().unwrap();
let triple = Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_name"),
Value::literal("Alice"),
);
assert!(!db.contains(&triple).unwrap());
db.insert(triple.clone()).unwrap();
assert!(db.contains(&triple).unwrap());
}
#[test]
fn test_get_subject() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_name"),
Value::literal("Alice"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_age"),
Value::integer(30),
))
.unwrap();
let results = db.get_subject(&NodeId::named("user:alice")).unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_get_predicate() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_name"),
Value::literal("Alice"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("user:bob"),
Predicate::named("has_name"),
Value::literal("Bob"),
))
.unwrap();
let results = db.get_predicate(&Predicate::named("has_name")).unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_get_object() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::new(
NodeId::named("user:alice"),
Predicate::named("has_title"),
Value::literal("Doctor"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("user:bob"),
Predicate::named("has_title"),
Value::literal("Doctor"),
))
.unwrap();
let results = db.get_object(&Value::literal("Doctor")).unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_graph_stats_default() {
let stats = GraphStats::default();
assert_eq!(stats.triple_count, 0);
assert_eq!(stats.subject_count, 0);
assert_eq!(stats.predicate_count, 0);
assert_eq!(stats.object_count, 0);
assert_eq!(stats.storage_bytes, 0);
}
#[test]
fn test_graph_stats_clone() {
let stats = GraphStats {
triple_count: 100,
subject_count: 50,
predicate_count: 10,
object_count: 75,
storage_bytes: 1024,
};
let cloned = stats.clone();
assert_eq!(cloned.triple_count, 100);
assert_eq!(cloned.subject_count, 50);
}
#[test]
fn test_graph_stats_debug() {
let stats = GraphStats {
triple_count: 10,
subject_count: 5,
predicate_count: 3,
object_count: 8,
storage_bytes: 512,
};
let debug_str = format!("{:?}", stats);
assert!(debug_str.contains("GraphStats"));
assert!(debug_str.contains("triple_count"));
}
#[test]
fn test_version_constant() {
assert!(!VERSION.is_empty());
}
#[test]
fn test_get_nonexistent() {
let db = GraphDB::memory().unwrap();
let triple = Triple::new(
NodeId::named("fake:node"),
Predicate::named("fake:pred"),
Value::literal("fake"),
);
let fake_id = triple.id();
let result = db.get(&fake_id).unwrap();
assert!(result.is_none());
}
#[test]
fn test_empty_query() {
let db = GraphDB::memory().unwrap();
let results = db.query().execute().unwrap();
assert!(results.is_empty());
}
#[test]
fn test_traverse_empty() {
let db = GraphDB::memory().unwrap();
let reachable = db
.traverse(&NodeId::named("nonexistent"), &[Predicate::named("knows")])
.unwrap();
assert!(reachable.is_empty());
}
#[test]
fn test_subjects_with_prefix() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::new(
NodeId::named("mayros:agent:alice"),
Predicate::named("has_name"),
Value::literal("Alice"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("mayros:agent:alice"),
Predicate::named("has_age"),
Value::integer(30),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("mayros:agent:bob"),
Predicate::named("has_name"),
Value::literal("Bob"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("other:node"),
Predicate::named("has_name"),
Value::literal("Other"),
))
.unwrap();
let subjects = db.subjects_with_prefix("mayros:agent:").unwrap();
assert_eq!(subjects.len(), 2);
assert!(subjects.contains(&NodeId::named("mayros:agent:alice")));
assert!(subjects.contains(&NodeId::named("mayros:agent:bob")));
let empty = db.subjects_with_prefix("nonexistent:").unwrap();
assert!(empty.is_empty());
let empty_db = GraphDB::memory().unwrap();
let empty_result = empty_db.subjects_with_prefix("any:").unwrap();
assert!(empty_result.is_empty());
}
#[test]
fn test_delete_by_subject_prefix() {
let db = GraphDB::memory().unwrap();
db.insert(Triple::new(
NodeId::named("sandbox:test:a"),
Predicate::named("p1"),
Value::literal("v1"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("sandbox:test:a"),
Predicate::named("p2"),
Value::literal("v2"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("sandbox:test:b"),
Predicate::named("p1"),
Value::literal("v3"),
))
.unwrap();
db.insert(Triple::new(
NodeId::named("keep:this"),
Predicate::named("p1"),
Value::literal("v4"),
))
.unwrap();
assert_eq!(db.count(), 4);
let deleted = db.delete_by_subject_prefix("sandbox:test:").unwrap();
assert_eq!(deleted, 3);
assert_eq!(db.count(), 1);
let remaining = db.get_subject(&NodeId::named("keep:this")).unwrap();
assert_eq!(remaining.len(), 1);
let deleted_none = db.delete_by_subject_prefix("nonexistent:").unwrap();
assert_eq!(deleted_none, 0);
}
#[cfg(feature = "dag")]
mod dag_tests {
use super::*;
#[test]
fn test_memory_with_dag() {
let db = GraphDB::memory_with_dag().unwrap();
assert!(db.dag_store().is_some());
assert_eq!(db.count(), 0);
}
#[test]
fn test_insert_via_dag() {
let db = GraphDB::memory_with_dag().unwrap();
let triple = Triple::new(
NodeId::named("alice"),
Predicate::named("knows"),
Value::literal("bob"),
);
let (dag_hash, triple_id) = db
.insert_via_dag(triple, NodeId::named("node:1"), 1, vec![])
.unwrap();
assert_eq!(db.count(), 1);
assert!(db.get(&triple_id).unwrap().is_some());
let action = db.dag_action(&dag_hash).unwrap().unwrap();
assert_eq!(action.seq, 1);
assert!(action.is_genesis() == false || action.parents.is_empty());
let tips = db.dag_tips().unwrap();
assert_eq!(tips.len(), 1);
assert_eq!(tips[0], dag_hash);
}
#[test]
fn test_insert_via_dag_same_materialized_state_as_insert() {
let db_plain = GraphDB::memory().unwrap();
let db_dag = GraphDB::memory_with_dag().unwrap();
let triple = Triple::new(
NodeId::named("alice"),
Predicate::named("knows"),
Value::literal("bob"),
);
let id_plain = db_plain.insert(triple.clone()).unwrap();
let (_, id_dag) = db_dag
.insert_via_dag(triple, NodeId::named("node:1"), 1, vec![])
.unwrap();
assert_eq!(id_plain, id_dag);
assert_eq!(db_plain.count(), db_dag.count());
}
#[test]
fn test_delete_via_dag() {
let db = GraphDB::memory_with_dag().unwrap();
let triple = Triple::new(
NodeId::named("alice"),
Predicate::named("knows"),
Value::literal("bob"),
);
let (h1, triple_id) = db
.insert_via_dag(triple, NodeId::named("node:1"), 1, vec![])
.unwrap();
let h2 = db
.delete_via_dag(&triple_id, NodeId::named("node:1"), 2, vec![h1])
.unwrap();
assert_eq!(db.count(), 0);
let store = db.dag_store().unwrap();
assert_eq!(store.action_count(), 2);
let tips = db.dag_tips().unwrap();
assert_eq!(tips.len(), 1);
assert_eq!(tips[0], h2);
}
#[test]
fn test_dag_chain() {
let db = GraphDB::memory_with_dag().unwrap();
for seq in 1..=5 {
let triple = Triple::new(
NodeId::named(&format!("node:{}", seq)),
Predicate::named("p"),
Value::integer(seq),
);
db.insert_via_dag(triple, NodeId::named("node:1"), seq as u64, vec![])
.unwrap();
}
let chain = db.dag_chain(&NodeId::named("node:1"), 10).unwrap();
assert_eq!(chain.len(), 5);
assert_eq!(chain[0].seq, 5);
}
#[test]
fn test_enable_dag() {
let mut db = GraphDB::memory().unwrap();
assert!(db.dag_store().is_none());
db.enable_dag();
assert!(db.dag_store().is_some());
}
}
}