1#![deny(clippy::all)]
7#![warn(clippy::pedantic)]
8
9use napi::bindgen_prelude::*;
10use napi_derive::napi;
11use ruvector_core::advanced::hypergraph::{
12 CausalMemory as CoreCausalMemory, Hyperedge as CoreHyperedge,
13 HypergraphIndex as CoreHypergraphIndex,
14};
15use ruvector_core::DistanceMetric;
16use ruvector_graph::cypher::{parse_cypher, Statement};
17use ruvector_graph::node::NodeBuilder;
18use ruvector_graph::storage::GraphStorage;
19use ruvector_graph::GraphDB;
20use std::sync::{Arc, RwLock};
21
22mod streaming;
23mod transactions;
24mod types;
25
26pub use streaming::*;
27pub use transactions::*;
28pub use types::*;
29
30#[napi]
32pub struct GraphDatabase {
33 hypergraph: Arc<RwLock<CoreHypergraphIndex>>,
34 causal_memory: Arc<RwLock<CoreCausalMemory>>,
35 transaction_manager: Arc<RwLock<transactions::TransactionManager>>,
36 graph_db: Arc<RwLock<GraphDB>>,
38 storage: Option<Arc<RwLock<GraphStorage>>>,
40 storage_path: Option<String>,
42}
43
44#[napi]
45impl GraphDatabase {
46 #[napi(constructor)]
56 pub fn new(options: Option<JsGraphOptions>) -> Result<Self> {
57 let opts = options.unwrap_or_default();
58 let metric = opts.distance_metric.unwrap_or(JsDistanceMetric::Cosine);
59 let core_metric: DistanceMetric = metric.into();
60
61 let (storage, storage_path) = if let Some(ref path) = opts.storage_path {
63 let gs = GraphStorage::new(path)
64 .map_err(|e| Error::from_reason(format!("Failed to open storage: {}", e)))?;
65 (Some(Arc::new(RwLock::new(gs))), Some(path.clone()))
66 } else {
67 (None, None)
68 };
69
70 Ok(Self {
71 hypergraph: Arc::new(RwLock::new(CoreHypergraphIndex::new(core_metric))),
72 causal_memory: Arc::new(RwLock::new(CoreCausalMemory::new(core_metric))),
73 transaction_manager: Arc::new(RwLock::new(transactions::TransactionManager::new())),
74 graph_db: Arc::new(RwLock::new(GraphDB::new())),
75 storage,
76 storage_path,
77 })
78 }
79
80 #[napi(factory)]
87 pub fn open(path: String) -> Result<Self> {
88 let storage = GraphStorage::new(&path)
89 .map_err(|e| Error::from_reason(format!("Failed to open storage: {}", e)))?;
90
91 let metric = DistanceMetric::Cosine;
92
93 Ok(Self {
94 hypergraph: Arc::new(RwLock::new(CoreHypergraphIndex::new(metric))),
95 causal_memory: Arc::new(RwLock::new(CoreCausalMemory::new(metric))),
96 transaction_manager: Arc::new(RwLock::new(transactions::TransactionManager::new())),
97 graph_db: Arc::new(RwLock::new(GraphDB::new())),
98 storage: Some(Arc::new(RwLock::new(storage))),
99 storage_path: Some(path),
100 })
101 }
102
103 #[napi]
112 pub fn is_persistent(&self) -> bool {
113 self.storage.is_some()
114 }
115
116 #[napi]
118 pub fn get_storage_path(&self) -> Option<String> {
119 self.storage_path.clone()
120 }
121
122 #[napi]
133 pub async fn create_node(&self, node: JsNode) -> Result<String> {
134 let hypergraph = self.hypergraph.clone();
135 let graph_db = self.graph_db.clone();
136 let storage = self.storage.clone();
137 let id = node.id.clone();
138 let embedding = node.embedding.to_vec();
139 let properties = node.properties.clone();
140 let labels = node.labels.clone();
141
142 tokio::task::spawn_blocking(move || {
143 let mut hg = hypergraph.write().expect("RwLock poisoned");
145 hg.add_entity(id.clone(), embedding);
146
147 let mut gdb = graph_db.write().expect("RwLock poisoned");
149 let mut builder = NodeBuilder::new().id(&id);
150
151 if let Some(node_labels) = labels {
153 for label in node_labels {
154 builder = builder.label(&label);
155 }
156 }
157
158 if let Some(props) = properties {
160 for (key, value) in props {
161 builder = builder.property(&key, value);
162 }
163 }
164
165 let graph_node = builder.build();
166
167 if let Some(ref storage_arc) = storage {
169 let storage_guard = storage_arc.write().expect("Storage RwLock poisoned");
170 storage_guard.insert_node(&graph_node)
171 .map_err(|e| Error::from_reason(format!("Failed to persist node: {}", e)))?;
172 }
173
174 gdb.create_node(graph_node)
175 .map_err(|e| Error::from_reason(format!("Failed to create node: {}", e)))?;
176
177 Ok::<String, Error>(id)
178 })
179 .await
180 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
181 }
182
183 #[napi]
196 pub async fn create_edge(&self, edge: JsEdge) -> Result<String> {
197 let hypergraph = self.hypergraph.clone();
198 let nodes = vec![edge.from.clone(), edge.to.clone()];
199 let description = edge.description.clone();
200 let embedding = edge.embedding.to_vec();
201 let confidence = edge.confidence.unwrap_or(1.0) as f32;
202
203 tokio::task::spawn_blocking(move || {
204 let core_edge = CoreHyperedge::new(nodes, description, embedding, confidence);
205 let edge_id = core_edge.id.clone();
206 let mut hg = hypergraph.write().expect("RwLock poisoned");
207 hg.add_hyperedge(core_edge)
208 .map_err(|e| Error::from_reason(format!("Failed to create edge: {}", e)))?;
209 Ok(edge_id)
210 })
211 .await
212 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
213 }
214
215 #[napi]
228 pub async fn create_hyperedge(&self, hyperedge: JsHyperedge) -> Result<String> {
229 let hypergraph = self.hypergraph.clone();
230 let nodes = hyperedge.nodes.clone();
231 let description = hyperedge.description.clone();
232 let embedding = hyperedge.embedding.to_vec();
233 let confidence = hyperedge.confidence.unwrap_or(1.0) as f32;
234
235 tokio::task::spawn_blocking(move || {
236 let core_edge = CoreHyperedge::new(nodes, description, embedding, confidence);
237 let edge_id = core_edge.id.clone();
238 let mut hg = hypergraph.write().expect("RwLock poisoned");
239 hg.add_hyperedge(core_edge)
240 .map_err(|e| Error::from_reason(format!("Failed to create hyperedge: {}", e)))?;
241 Ok(edge_id)
242 })
243 .await
244 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
245 }
246
247 #[napi]
254 pub async fn query(&self, cypher: String) -> Result<JsQueryResult> {
255 let graph_db = self.graph_db.clone();
256 let hypergraph = self.hypergraph.clone();
257
258 tokio::task::spawn_blocking(move || {
259 let parsed = parse_cypher(&cypher)
261 .map_err(|e| Error::from_reason(format!("Cypher parse error: {}", e)))?;
262
263 let gdb = graph_db.read().expect("RwLock poisoned");
264 let hg = hypergraph.read().expect("RwLock poisoned");
265
266 let mut result_nodes: Vec<JsNodeResult> = Vec::new();
267 let mut result_edges: Vec<JsEdgeResult> = Vec::new();
268
269 for statement in &parsed.statements {
271 match statement {
272 Statement::Match(match_clause) => {
273 for pattern in &match_clause.patterns {
275 if let ruvector_graph::cypher::ast::Pattern::Node(node_pattern) = pattern {
276 for label in &node_pattern.labels {
277 let nodes = gdb.get_nodes_by_label(label);
278 for node in nodes {
279 result_nodes.push(JsNodeResult {
280 id: node.id.clone(),
281 labels: node.labels.iter().map(|l| l.name.clone()).collect(),
282 properties: node.properties.iter()
283 .map(|(k, v)| (k.clone(), format!("{:?}", v)))
284 .collect(),
285 });
286 }
287 }
288 if node_pattern.labels.is_empty() && node_pattern.variable.is_some() {
290 }
292 }
293 }
294 }
295 Statement::Create(create_clause) => {
296 }
298 Statement::Return(_) => {
299 }
301 _ => {}
302 }
303 }
304
305 let stats = hg.stats();
306
307 Ok::<JsQueryResult, Error>(JsQueryResult {
308 nodes: result_nodes,
309 edges: result_edges,
310 stats: Some(JsGraphStats {
311 total_nodes: stats.total_entities as u32,
312 total_edges: stats.total_hyperedges as u32,
313 avg_degree: stats.avg_entity_degree as f64,
314 }),
315 })
316 })
317 .await
318 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
319 }
320
321 #[napi]
328 pub fn query_sync(&self, cypher: String) -> Result<JsQueryResult> {
329 let hg = self.hypergraph.read().expect("RwLock poisoned");
330 let stats = hg.stats();
331
332 Ok(JsQueryResult {
334 nodes: vec![],
335 edges: vec![],
336 stats: Some(JsGraphStats {
337 total_nodes: stats.total_entities as u32,
338 total_edges: stats.total_hyperedges as u32,
339 avg_degree: stats.avg_entity_degree as f64,
340 }),
341 })
342 }
343
344 #[napi]
354 pub async fn search_hyperedges(
355 &self,
356 query: JsHyperedgeQuery,
357 ) -> Result<Vec<JsHyperedgeResult>> {
358 let hypergraph = self.hypergraph.clone();
359 let embedding = query.embedding.to_vec();
360 let k = query.k as usize;
361
362 tokio::task::spawn_blocking(move || {
363 let hg = hypergraph.read().expect("RwLock poisoned");
364 let results = hg.search_hyperedges(&embedding, k);
365
366 Ok::<Vec<JsHyperedgeResult>, Error>(
367 results
368 .into_iter()
369 .map(|(id, score)| JsHyperedgeResult {
370 id,
371 score: f64::from(score),
372 })
373 .collect(),
374 )
375 })
376 .await
377 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
378 }
379
380 #[napi]
387 pub async fn k_hop_neighbors(&self, start_node: String, k: u32) -> Result<Vec<String>> {
388 let hypergraph = self.hypergraph.clone();
389 let hops = k as usize;
390
391 tokio::task::spawn_blocking(move || {
392 let hg = hypergraph.read().expect("RwLock poisoned");
393 let neighbors = hg.k_hop_neighbors(start_node, hops);
394 Ok::<Vec<String>, Error>(neighbors.into_iter().collect())
395 })
396 .await
397 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
398 }
399
400 #[napi]
407 pub async fn begin(&self) -> Result<String> {
408 let tm = self.transaction_manager.clone();
409
410 tokio::task::spawn_blocking(move || {
411 let mut manager = tm.write().expect("RwLock poisoned");
412 Ok::<String, Error>(manager.begin())
413 })
414 .await
415 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
416 }
417
418 #[napi]
425 pub async fn commit(&self, tx_id: String) -> Result<()> {
426 let tm = self.transaction_manager.clone();
427
428 tokio::task::spawn_blocking(move || {
429 let mut manager = tm.write().expect("RwLock poisoned");
430 manager
431 .commit(&tx_id)
432 .map_err(|e| Error::from_reason(format!("Failed to commit: {}", e)))
433 })
434 .await
435 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
436 }
437
438 #[napi]
445 pub async fn rollback(&self, tx_id: String) -> Result<()> {
446 let tm = self.transaction_manager.clone();
447
448 tokio::task::spawn_blocking(move || {
449 let mut manager = tm.write().expect("RwLock poisoned");
450 manager
451 .rollback(&tx_id)
452 .map_err(|e| Error::from_reason(format!("Failed to rollback: {}", e)))
453 })
454 .await
455 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
456 }
457
458 #[napi]
468 pub async fn batch_insert(&self, batch: JsBatchInsert) -> Result<JsBatchResult> {
469 let hypergraph = self.hypergraph.clone();
470 let nodes = batch.nodes;
471 let edges = batch.edges;
472
473 tokio::task::spawn_blocking(move || {
474 let mut hg = hypergraph.write().expect("RwLock poisoned");
475 let mut node_ids = Vec::new();
476 let mut edge_ids = Vec::new();
477
478 for node in nodes {
480 hg.add_entity(node.id.clone(), node.embedding.to_vec());
481 node_ids.push(node.id);
482 }
483
484 for edge in edges {
486 let nodes = vec![edge.from.clone(), edge.to.clone()];
487 let embedding = edge.embedding.to_vec();
488 let confidence = edge.confidence.unwrap_or(1.0) as f32;
489 let core_edge = CoreHyperedge::new(nodes, edge.description, embedding, confidence);
490 let edge_id = core_edge.id.clone();
491 hg.add_hyperedge(core_edge)
492 .map_err(|e| Error::from_reason(format!("Failed to insert edge: {}", e)))?;
493 edge_ids.push(edge_id);
494 }
495
496 Ok::<JsBatchResult, Error>(JsBatchResult { node_ids, edge_ids })
497 })
498 .await
499 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
500 }
501
502 #[napi]
511 pub fn subscribe(&self, callback: JsFunction) -> Result<()> {
512 Ok(())
515 }
516
517 #[napi]
525 pub async fn stats(&self) -> Result<JsGraphStats> {
526 let hypergraph = self.hypergraph.clone();
527
528 tokio::task::spawn_blocking(move || {
529 let hg = hypergraph.read().expect("RwLock poisoned");
530 let stats = hg.stats();
531
532 Ok::<JsGraphStats, Error>(JsGraphStats {
533 total_nodes: stats.total_entities as u32,
534 total_edges: stats.total_hyperedges as u32,
535 avg_degree: stats.avg_entity_degree as f64,
536 })
537 })
538 .await
539 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
540 }
541}
542
543#[napi]
545pub fn version() -> String {
546 env!("CARGO_PKG_VERSION").to_string()
547}
548
549#[napi]
551pub fn hello() -> String {
552 "Hello from RuVector Graph Node.js bindings!".to_string()
553}