ruvector_graph_node/
lib.rs

1//! Node.js bindings for RuVector Graph Database via NAPI-RS
2//!
3//! High-performance native graph database with Cypher-like query support,
4//! hypergraph capabilities, async/await support, and zero-copy buffer sharing.
5
6#![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/// Graph database for complex relationship queries
31#[napi]
32pub struct GraphDatabase {
33    hypergraph: Arc<RwLock<CoreHypergraphIndex>>,
34    causal_memory: Arc<RwLock<CoreCausalMemory>>,
35    transaction_manager: Arc<RwLock<transactions::TransactionManager>>,
36    /// Property graph database with Cypher support
37    graph_db: Arc<RwLock<GraphDB>>,
38    /// Persistent storage backend (optional)
39    storage: Option<Arc<RwLock<GraphStorage>>>,
40    /// Path to storage file (if persisted)
41    storage_path: Option<String>,
42}
43
44#[napi]
45impl GraphDatabase {
46    /// Create a new graph database
47    ///
48    /// # Example
49    /// ```javascript
50    /// const db = new GraphDatabase({
51    ///   distanceMetric: 'Cosine',
52    ///   dimensions: 384
53    /// });
54    /// ```
55    #[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        // Check if storage path is provided for persistence
62        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    /// Open an existing graph database from disk
81    ///
82    /// # Example
83    /// ```javascript
84    /// const db = GraphDatabase.open('./my-graph.db');
85    /// ```
86    #[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    /// Check if persistence is enabled
104    ///
105    /// # Example
106    /// ```javascript
107    /// if (db.isPersistent()) {
108    ///   console.log('Data is being saved to:', db.getStoragePath());
109    /// }
110    /// ```
111    #[napi]
112    pub fn is_persistent(&self) -> bool {
113        self.storage.is_some()
114    }
115
116    /// Get the storage path (if persisted)
117    #[napi]
118    pub fn get_storage_path(&self) -> Option<String> {
119        self.storage_path.clone()
120    }
121
122    /// Create a node in the graph
123    ///
124    /// # Example
125    /// ```javascript
126    /// const nodeId = await db.createNode({
127    ///   id: 'node1',
128    ///   embedding: new Float32Array([1, 2, 3]),
129    ///   properties: { name: 'Alice', age: 30 }
130    /// });
131    /// ```
132    #[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            // Add to hypergraph index
144            let mut hg = hypergraph.write().expect("RwLock poisoned");
145            hg.add_entity(id.clone(), embedding);
146
147            // Add to property graph
148            let mut gdb = graph_db.write().expect("RwLock poisoned");
149            let mut builder = NodeBuilder::new().id(&id);
150
151            // Add labels if provided
152            if let Some(node_labels) = labels {
153                for label in node_labels {
154                    builder = builder.label(&label);
155                }
156            }
157
158            // Add properties if provided
159            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            // Persist to storage if enabled
168            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    /// Create an edge between two nodes
184    ///
185    /// # Example
186    /// ```javascript
187    /// const edgeId = await db.createEdge({
188    ///   from: 'node1',
189    ///   to: 'node2',
190    ///   description: 'knows',
191    ///   embedding: new Float32Array([0.5, 0.5, 0.5]),
192    ///   confidence: 0.95
193    /// });
194    /// ```
195    #[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    /// Create a hyperedge connecting multiple nodes
216    ///
217    /// # Example
218    /// ```javascript
219    /// const hyperedgeId = await db.createHyperedge({
220    ///   nodes: ['node1', 'node2', 'node3'],
221    ///   description: 'collaborated_on_project',
222    ///   embedding: new Float32Array([0.3, 0.6, 0.9]),
223    ///   confidence: 0.85,
224    ///   metadata: { project: 'AI Research' }
225    /// });
226    /// ```
227    #[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    /// Query the graph using Cypher-like syntax
248    ///
249    /// # Example
250    /// ```javascript
251    /// const results = await db.query('MATCH (n) RETURN n LIMIT 10');
252    /// ```
253    #[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            // Parse the Cypher query
260            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            // Execute each statement
270            for statement in &parsed.statements {
271                match statement {
272                    Statement::Match(match_clause) => {
273                        // Extract label from match patterns for query
274                        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 no labels specified, return all nodes (simplified)
289                                if node_pattern.labels.is_empty() && node_pattern.variable.is_some() {
290                                    // This would need iteration over all nodes - for now just stats
291                                }
292                            }
293                        }
294                    }
295                    Statement::Create(create_clause) => {
296                        // Handle CREATE - but we need mutable access, so skip in query
297                    }
298                    Statement::Return(_) => {
299                        // RETURN is handled implicitly
300                    }
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    /// Query the graph synchronously
322    ///
323    /// # Example
324    /// ```javascript
325    /// const results = db.querySync('MATCH (n) RETURN n LIMIT 10');
326    /// ```
327    #[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        // Simplified query result for now
333        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    /// Search for similar hyperedges
345    ///
346    /// # Example
347    /// ```javascript
348    /// const results = await db.searchHyperedges({
349    ///   embedding: new Float32Array([0.5, 0.5, 0.5]),
350    ///   k: 10
351    /// });
352    /// ```
353    #[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    /// Get k-hop neighbors from a starting node
381    ///
382    /// # Example
383    /// ```javascript
384    /// const neighbors = await db.kHopNeighbors('node1', 2);
385    /// ```
386    #[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    /// Begin a new transaction
401    ///
402    /// # Example
403    /// ```javascript
404    /// const txId = await db.begin();
405    /// ```
406    #[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    /// Commit a transaction
419    ///
420    /// # Example
421    /// ```javascript
422    /// await db.commit(txId);
423    /// ```
424    #[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    /// Rollback a transaction
439    ///
440    /// # Example
441    /// ```javascript
442    /// await db.rollback(txId);
443    /// ```
444    #[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    /// Batch insert nodes and edges
459    ///
460    /// # Example
461    /// ```javascript
462    /// await db.batchInsert({
463    ///   nodes: [{ id: 'n1', embedding: new Float32Array([1, 2]) }],
464    ///   edges: [{ from: 'n1', to: 'n2', description: 'knows' }]
465    /// });
466    /// ```
467    #[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            // Insert nodes
479            for node in nodes {
480                hg.add_entity(node.id.clone(), node.embedding.to_vec());
481                node_ids.push(node.id);
482            }
483
484            // Insert edges
485            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    /// Subscribe to graph changes (returns a change stream)
503    ///
504    /// # Example
505    /// ```javascript
506    /// const unsubscribe = db.subscribe((change) => {
507    ///   console.log('Graph changed:', change);
508    /// });
509    /// ```
510    #[napi]
511    pub fn subscribe(&self, callback: JsFunction) -> Result<()> {
512        // Placeholder for event emitter pattern
513        // In a real implementation, this would set up a change listener
514        Ok(())
515    }
516
517    /// Get graph statistics
518    ///
519    /// # Example
520    /// ```javascript
521    /// const stats = await db.stats();
522    /// console.log(`Nodes: ${stats.totalNodes}, Edges: ${stats.totalEdges}`);
523    /// ```
524    #[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/// Get the version of the library
544#[napi]
545pub fn version() -> String {
546    env!("CARGO_PKG_VERSION").to_string()
547}
548
549/// Test function to verify bindings
550#[napi]
551pub fn hello() -> String {
552    "Hello from RuVector Graph Node.js bindings!".to_string()
553}