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
171                    .insert_node(&graph_node)
172                    .map_err(|e| Error::from_reason(format!("Failed to persist node: {}", e)))?;
173            }
174
175            gdb.create_node(graph_node)
176                .map_err(|e| Error::from_reason(format!("Failed to create node: {}", e)))?;
177
178            Ok::<String, Error>(id)
179        })
180        .await
181        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
182    }
183
184    /// Create an edge between two nodes
185    ///
186    /// # Example
187    /// ```javascript
188    /// const edgeId = await db.createEdge({
189    ///   from: 'node1',
190    ///   to: 'node2',
191    ///   description: 'knows',
192    ///   embedding: new Float32Array([0.5, 0.5, 0.5]),
193    ///   confidence: 0.95
194    /// });
195    /// ```
196    #[napi]
197    pub async fn create_edge(&self, edge: JsEdge) -> Result<String> {
198        let hypergraph = self.hypergraph.clone();
199        let nodes = vec![edge.from.clone(), edge.to.clone()];
200        let description = edge.description.clone();
201        let embedding = edge.embedding.to_vec();
202        let confidence = edge.confidence.unwrap_or(1.0) as f32;
203
204        tokio::task::spawn_blocking(move || {
205            let core_edge = CoreHyperedge::new(nodes, description, embedding, confidence);
206            let edge_id = core_edge.id.clone();
207            let mut hg = hypergraph.write().expect("RwLock poisoned");
208            hg.add_hyperedge(core_edge)
209                .map_err(|e| Error::from_reason(format!("Failed to create edge: {}", e)))?;
210            Ok(edge_id)
211        })
212        .await
213        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
214    }
215
216    /// Create a hyperedge connecting multiple nodes
217    ///
218    /// # Example
219    /// ```javascript
220    /// const hyperedgeId = await db.createHyperedge({
221    ///   nodes: ['node1', 'node2', 'node3'],
222    ///   description: 'collaborated_on_project',
223    ///   embedding: new Float32Array([0.3, 0.6, 0.9]),
224    ///   confidence: 0.85,
225    ///   metadata: { project: 'AI Research' }
226    /// });
227    /// ```
228    #[napi]
229    pub async fn create_hyperedge(&self, hyperedge: JsHyperedge) -> Result<String> {
230        let hypergraph = self.hypergraph.clone();
231        let nodes = hyperedge.nodes.clone();
232        let description = hyperedge.description.clone();
233        let embedding = hyperedge.embedding.to_vec();
234        let confidence = hyperedge.confidence.unwrap_or(1.0) as f32;
235
236        tokio::task::spawn_blocking(move || {
237            let core_edge = CoreHyperedge::new(nodes, description, embedding, confidence);
238            let edge_id = core_edge.id.clone();
239            let mut hg = hypergraph.write().expect("RwLock poisoned");
240            hg.add_hyperedge(core_edge)
241                .map_err(|e| Error::from_reason(format!("Failed to create hyperedge: {}", e)))?;
242            Ok(edge_id)
243        })
244        .await
245        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
246    }
247
248    /// Query the graph using Cypher-like syntax
249    ///
250    /// # Example
251    /// ```javascript
252    /// const results = await db.query('MATCH (n) RETURN n LIMIT 10');
253    /// ```
254    #[napi]
255    pub async fn query(&self, cypher: String) -> Result<JsQueryResult> {
256        let graph_db = self.graph_db.clone();
257        let hypergraph = self.hypergraph.clone();
258
259        tokio::task::spawn_blocking(move || {
260            // Parse the Cypher query
261            let parsed = parse_cypher(&cypher)
262                .map_err(|e| Error::from_reason(format!("Cypher parse error: {}", e)))?;
263
264            let gdb = graph_db.read().expect("RwLock poisoned");
265            let hg = hypergraph.read().expect("RwLock poisoned");
266
267            let mut result_nodes: Vec<JsNodeResult> = Vec::new();
268            let mut result_edges: Vec<JsEdgeResult> = Vec::new();
269
270            // Execute each statement
271            for statement in &parsed.statements {
272                match statement {
273                    Statement::Match(match_clause) => {
274                        // Extract label from match patterns for query
275                        for pattern in &match_clause.patterns {
276                            if let ruvector_graph::cypher::ast::Pattern::Node(node_pattern) =
277                                pattern
278                            {
279                                for label in &node_pattern.labels {
280                                    let nodes = gdb.get_nodes_by_label(label);
281                                    for node in nodes {
282                                        result_nodes.push(JsNodeResult {
283                                            id: node.id.clone(),
284                                            labels: node
285                                                .labels
286                                                .iter()
287                                                .map(|l| l.name.clone())
288                                                .collect(),
289                                            properties: node
290                                                .properties
291                                                .iter()
292                                                .map(|(k, v)| (k.clone(), format!("{:?}", v)))
293                                                .collect(),
294                                        });
295                                    }
296                                }
297                                // If no labels specified, return all nodes (simplified)
298                                if node_pattern.labels.is_empty() && node_pattern.variable.is_some()
299                                {
300                                    // This would need iteration over all nodes - for now just stats
301                                }
302                            }
303                        }
304                    }
305                    Statement::Create(create_clause) => {
306                        // Handle CREATE - but we need mutable access, so skip in query
307                    }
308                    Statement::Return(_) => {
309                        // RETURN is handled implicitly
310                    }
311                    _ => {}
312                }
313            }
314
315            let stats = hg.stats();
316
317            Ok::<JsQueryResult, Error>(JsQueryResult {
318                nodes: result_nodes,
319                edges: result_edges,
320                stats: Some(JsGraphStats {
321                    total_nodes: stats.total_entities as u32,
322                    total_edges: stats.total_hyperedges as u32,
323                    avg_degree: stats.avg_entity_degree as f64,
324                }),
325            })
326        })
327        .await
328        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
329    }
330
331    /// Query the graph synchronously
332    ///
333    /// # Example
334    /// ```javascript
335    /// const results = db.querySync('MATCH (n) RETURN n LIMIT 10');
336    /// ```
337    #[napi]
338    pub fn query_sync(&self, cypher: String) -> Result<JsQueryResult> {
339        let hg = self.hypergraph.read().expect("RwLock poisoned");
340        let stats = hg.stats();
341
342        // Simplified query result for now
343        Ok(JsQueryResult {
344            nodes: vec![],
345            edges: vec![],
346            stats: Some(JsGraphStats {
347                total_nodes: stats.total_entities as u32,
348                total_edges: stats.total_hyperedges as u32,
349                avg_degree: stats.avg_entity_degree as f64,
350            }),
351        })
352    }
353
354    /// Search for similar hyperedges
355    ///
356    /// # Example
357    /// ```javascript
358    /// const results = await db.searchHyperedges({
359    ///   embedding: new Float32Array([0.5, 0.5, 0.5]),
360    ///   k: 10
361    /// });
362    /// ```
363    #[napi]
364    pub async fn search_hyperedges(
365        &self,
366        query: JsHyperedgeQuery,
367    ) -> Result<Vec<JsHyperedgeResult>> {
368        let hypergraph = self.hypergraph.clone();
369        let embedding = query.embedding.to_vec();
370        let k = query.k as usize;
371
372        tokio::task::spawn_blocking(move || {
373            let hg = hypergraph.read().expect("RwLock poisoned");
374            let results = hg.search_hyperedges(&embedding, k);
375
376            Ok::<Vec<JsHyperedgeResult>, Error>(
377                results
378                    .into_iter()
379                    .map(|(id, score)| JsHyperedgeResult {
380                        id,
381                        score: f64::from(score),
382                    })
383                    .collect(),
384            )
385        })
386        .await
387        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
388    }
389
390    /// Get k-hop neighbors from a starting node
391    ///
392    /// # Example
393    /// ```javascript
394    /// const neighbors = await db.kHopNeighbors('node1', 2);
395    /// ```
396    #[napi]
397    pub async fn k_hop_neighbors(&self, start_node: String, k: u32) -> Result<Vec<String>> {
398        let hypergraph = self.hypergraph.clone();
399        let hops = k as usize;
400
401        tokio::task::spawn_blocking(move || {
402            let hg = hypergraph.read().expect("RwLock poisoned");
403            let neighbors = hg.k_hop_neighbors(start_node, hops);
404            Ok::<Vec<String>, Error>(neighbors.into_iter().collect())
405        })
406        .await
407        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
408    }
409
410    /// Begin a new transaction
411    ///
412    /// # Example
413    /// ```javascript
414    /// const txId = await db.begin();
415    /// ```
416    #[napi]
417    pub async fn begin(&self) -> Result<String> {
418        let tm = self.transaction_manager.clone();
419
420        tokio::task::spawn_blocking(move || {
421            let mut manager = tm.write().expect("RwLock poisoned");
422            Ok::<String, Error>(manager.begin())
423        })
424        .await
425        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
426    }
427
428    /// Commit a transaction
429    ///
430    /// # Example
431    /// ```javascript
432    /// await db.commit(txId);
433    /// ```
434    #[napi]
435    pub async fn commit(&self, tx_id: String) -> Result<()> {
436        let tm = self.transaction_manager.clone();
437
438        tokio::task::spawn_blocking(move || {
439            let mut manager = tm.write().expect("RwLock poisoned");
440            manager
441                .commit(&tx_id)
442                .map_err(|e| Error::from_reason(format!("Failed to commit: {}", e)))
443        })
444        .await
445        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
446    }
447
448    /// Rollback a transaction
449    ///
450    /// # Example
451    /// ```javascript
452    /// await db.rollback(txId);
453    /// ```
454    #[napi]
455    pub async fn rollback(&self, tx_id: String) -> Result<()> {
456        let tm = self.transaction_manager.clone();
457
458        tokio::task::spawn_blocking(move || {
459            let mut manager = tm.write().expect("RwLock poisoned");
460            manager
461                .rollback(&tx_id)
462                .map_err(|e| Error::from_reason(format!("Failed to rollback: {}", e)))
463        })
464        .await
465        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
466    }
467
468    /// Batch insert nodes and edges
469    ///
470    /// # Example
471    /// ```javascript
472    /// await db.batchInsert({
473    ///   nodes: [{ id: 'n1', embedding: new Float32Array([1, 2]) }],
474    ///   edges: [{ from: 'n1', to: 'n2', description: 'knows' }]
475    /// });
476    /// ```
477    #[napi]
478    pub async fn batch_insert(&self, batch: JsBatchInsert) -> Result<JsBatchResult> {
479        let hypergraph = self.hypergraph.clone();
480        let nodes = batch.nodes;
481        let edges = batch.edges;
482
483        tokio::task::spawn_blocking(move || {
484            let mut hg = hypergraph.write().expect("RwLock poisoned");
485            let mut node_ids = Vec::new();
486            let mut edge_ids = Vec::new();
487
488            // Insert nodes
489            for node in nodes {
490                hg.add_entity(node.id.clone(), node.embedding.to_vec());
491                node_ids.push(node.id);
492            }
493
494            // Insert edges
495            for edge in edges {
496                let nodes = vec![edge.from.clone(), edge.to.clone()];
497                let embedding = edge.embedding.to_vec();
498                let confidence = edge.confidence.unwrap_or(1.0) as f32;
499                let core_edge = CoreHyperedge::new(nodes, edge.description, embedding, confidence);
500                let edge_id = core_edge.id.clone();
501                hg.add_hyperedge(core_edge)
502                    .map_err(|e| Error::from_reason(format!("Failed to insert edge: {}", e)))?;
503                edge_ids.push(edge_id);
504            }
505
506            Ok::<JsBatchResult, Error>(JsBatchResult { node_ids, edge_ids })
507        })
508        .await
509        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
510    }
511
512    /// Subscribe to graph changes (returns a change stream)
513    ///
514    /// # Example
515    /// ```javascript
516    /// const unsubscribe = db.subscribe((change) => {
517    ///   console.log('Graph changed:', change);
518    /// });
519    /// ```
520    #[napi]
521    pub fn subscribe(&self, callback: JsFunction) -> Result<()> {
522        // Placeholder for event emitter pattern
523        // In a real implementation, this would set up a change listener
524        Ok(())
525    }
526
527    /// Get graph statistics
528    ///
529    /// # Example
530    /// ```javascript
531    /// const stats = await db.stats();
532    /// console.log(`Nodes: ${stats.totalNodes}, Edges: ${stats.totalEdges}`);
533    /// ```
534    #[napi]
535    pub async fn stats(&self) -> Result<JsGraphStats> {
536        let hypergraph = self.hypergraph.clone();
537
538        tokio::task::spawn_blocking(move || {
539            let hg = hypergraph.read().expect("RwLock poisoned");
540            let stats = hg.stats();
541
542            Ok::<JsGraphStats, Error>(JsGraphStats {
543                total_nodes: stats.total_entities as u32,
544                total_edges: stats.total_hyperedges as u32,
545                avg_degree: stats.avg_entity_degree as f64,
546            })
547        })
548        .await
549        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
550    }
551}
552
553/// Get the version of the library
554#[napi]
555pub fn version() -> String {
556    env!("CARGO_PKG_VERSION").to_string()
557}
558
559/// Test function to verify bindings
560#[napi]
561pub fn hello() -> String {
562    "Hello from RuVector Graph Node.js bindings!".to_string()
563}