mirror of
https://github.com/ruvnet/RuView
synced 2026-08-02 19:11:46 +00:00
889 lines
26 KiB
Rust
889 lines
26 KiB
Rust
//! RvLite - Standalone vector database with SQL, SPARQL, and Cypher
|
|
//!
|
|
//! A WASM-compatible vector database powered by RuVector.
|
|
//!
|
|
//! # Features
|
|
//! - Vector storage and similarity search
|
|
//! - SQL queries with pgvector-compatible syntax
|
|
//! - SPARQL queries for RDF data
|
|
//! - Cypher queries for property graphs
|
|
//! - IndexedDB persistence for browsers
|
|
//!
|
|
//! # Example (JavaScript)
|
|
//! ```javascript
|
|
//! import init, { RvLite, RvLiteConfig } from './rvlite.js';
|
|
//!
|
|
//! await init();
|
|
//! const config = new RvLiteConfig(384);
|
|
//! const db = new RvLite(config);
|
|
//!
|
|
//! // Insert vectors
|
|
//! db.insert([0.1, 0.2, 0.3, ...], { label: "test" });
|
|
//!
|
|
//! // Search
|
|
//! const results = db.search([0.1, 0.2, 0.3, ...], 10);
|
|
//!
|
|
//! // Cypher queries
|
|
//! db.cypher("CREATE (n:Person {name: 'Alice'})");
|
|
//!
|
|
//! // SPARQL queries
|
|
//! db.add_triple("<http://example.org/a>", "<http://example.org/knows>", "<http://example.org/b>");
|
|
//! db.sparql("SELECT ?s WHERE { ?s <http://example.org/knows> ?o }");
|
|
//!
|
|
//! // Persistence
|
|
//! await db.save(); // Save to IndexedDB
|
|
//! const db2 = await RvLite.load(config); // Load from IndexedDB
|
|
//! ```
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use wasm_bindgen::prelude::*;
|
|
use wasm_bindgen_futures::future_to_promise;
|
|
|
|
// Import ruvector-core
|
|
use ruvector_core::types::DbOptions;
|
|
use ruvector_core::{DistanceMetric, SearchQuery, VectorDB, VectorEntry};
|
|
|
|
// Query language modules
|
|
pub mod cypher;
|
|
pub mod sparql;
|
|
pub mod sql;
|
|
pub mod storage;
|
|
|
|
// Re-export storage types
|
|
pub use storage::{GraphState, RvLiteState, TripleStoreState, VectorState};
|
|
|
|
#[wasm_bindgen(start)]
|
|
pub fn init() {
|
|
console_error_panic_hook::set_once();
|
|
web_sys::console::log_1(&"RvLite v0.2.0 - SQL, SPARQL, Cypher + Persistence".into());
|
|
}
|
|
|
|
/// Error type for RvLite
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RvLiteError {
|
|
pub message: String,
|
|
pub kind: ErrorKind,
|
|
}
|
|
|
|
impl std::fmt::Display for RvLiteError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{:?}: {}", self.kind, self.message)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for RvLiteError {}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum ErrorKind {
|
|
VectorError,
|
|
SqlError,
|
|
CypherError,
|
|
SparqlError,
|
|
StorageError,
|
|
WasmError,
|
|
NotImplemented,
|
|
}
|
|
|
|
impl From<ruvector_core::RuvectorError> for RvLiteError {
|
|
fn from(e: ruvector_core::RuvectorError) -> Self {
|
|
RvLiteError {
|
|
message: e.to_string(),
|
|
kind: ErrorKind::VectorError,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<RvLiteError> for JsValue {
|
|
fn from(e: RvLiteError) -> Self {
|
|
serde_wasm_bindgen::to_value(&e).unwrap_or_else(|_| JsValue::from_str(&e.message))
|
|
}
|
|
}
|
|
|
|
impl From<sparql::SparqlError> for RvLiteError {
|
|
fn from(e: sparql::SparqlError) -> Self {
|
|
RvLiteError {
|
|
message: e.to_string(),
|
|
kind: ErrorKind::SparqlError,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<sql::ParseError> for RvLiteError {
|
|
fn from(e: sql::ParseError) -> Self {
|
|
RvLiteError {
|
|
message: e.to_string(),
|
|
kind: ErrorKind::SqlError,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration for RvLite database
|
|
#[wasm_bindgen]
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct RvLiteConfig {
|
|
/// Vector dimensions
|
|
dimensions: usize,
|
|
/// Distance metric (euclidean, cosine, dotproduct, manhattan)
|
|
distance_metric: String,
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
impl RvLiteConfig {
|
|
#[wasm_bindgen(constructor)]
|
|
pub fn new(dimensions: usize) -> Self {
|
|
RvLiteConfig {
|
|
dimensions,
|
|
distance_metric: "cosine".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Set distance metric (euclidean, cosine, dotproduct, manhattan)
|
|
pub fn with_distance_metric(mut self, metric: String) -> Self {
|
|
self.distance_metric = metric;
|
|
self
|
|
}
|
|
|
|
/// Get dimensions
|
|
pub fn get_dimensions(&self) -> usize {
|
|
self.dimensions
|
|
}
|
|
|
|
/// Get distance metric name
|
|
pub fn get_distance_metric(&self) -> String {
|
|
self.distance_metric.clone()
|
|
}
|
|
}
|
|
|
|
impl RvLiteConfig {
|
|
fn to_db_options(&self) -> DbOptions {
|
|
let metric = match self.distance_metric.to_lowercase().as_str() {
|
|
"euclidean" => DistanceMetric::Euclidean,
|
|
"cosine" => DistanceMetric::Cosine,
|
|
"dotproduct" => DistanceMetric::DotProduct,
|
|
"manhattan" => DistanceMetric::Manhattan,
|
|
_ => DistanceMetric::Cosine,
|
|
};
|
|
|
|
DbOptions {
|
|
dimensions: self.dimensions,
|
|
distance_metric: metric,
|
|
storage_path: "memory://".to_string(),
|
|
hnsw_config: None,
|
|
quantization: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Main RvLite database
|
|
#[wasm_bindgen]
|
|
pub struct RvLite {
|
|
db: VectorDB,
|
|
config: RvLiteConfig,
|
|
cypher_engine: cypher::CypherEngine,
|
|
sql_engine: sql::SqlEngine,
|
|
triple_store: sparql::TripleStore,
|
|
storage: Option<storage::IndexedDBStorage>,
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
impl RvLite {
|
|
/// Create a new RvLite database
|
|
#[wasm_bindgen(constructor)]
|
|
pub fn new(config: RvLiteConfig) -> Result<RvLite, JsValue> {
|
|
let db = VectorDB::new(config.to_db_options()).map_err(|e| RvLiteError::from(e))?;
|
|
|
|
Ok(RvLite {
|
|
db,
|
|
config,
|
|
cypher_engine: cypher::CypherEngine::new(),
|
|
sql_engine: sql::SqlEngine::new(),
|
|
triple_store: sparql::TripleStore::new(),
|
|
storage: None,
|
|
})
|
|
}
|
|
|
|
/// Create with default configuration (384 dimensions, cosine similarity)
|
|
pub fn default() -> Result<RvLite, JsValue> {
|
|
Self::new(RvLiteConfig::new(384))
|
|
}
|
|
|
|
/// Check if database is ready
|
|
pub fn is_ready(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
/// Get version string
|
|
pub fn get_version(&self) -> String {
|
|
"0.2.0".to_string()
|
|
}
|
|
|
|
/// Get enabled features
|
|
pub fn get_features(&self) -> Result<JsValue, JsValue> {
|
|
let features = vec![
|
|
"core",
|
|
"vectors",
|
|
"search",
|
|
"sql",
|
|
"sparql",
|
|
"cypher",
|
|
"memory-storage",
|
|
"indexeddb-persistence",
|
|
];
|
|
serde_wasm_bindgen::to_value(&features).map_err(|e| JsValue::from_str(&e.to_string()))
|
|
}
|
|
|
|
// ===== Persistence Methods =====
|
|
|
|
/// Initialize IndexedDB storage for persistence
|
|
/// Must be called before save() or load()
|
|
pub fn init_storage(&mut self) -> js_sys::Promise {
|
|
let mut storage = storage::IndexedDBStorage::new();
|
|
|
|
future_to_promise(async move {
|
|
storage.init().await?;
|
|
Ok(JsValue::TRUE)
|
|
})
|
|
}
|
|
|
|
/// Check if IndexedDB is available in the browser
|
|
pub fn is_storage_available() -> bool {
|
|
storage::IndexedDBStorage::is_available()
|
|
}
|
|
|
|
/// Save database state to IndexedDB
|
|
/// Returns a Promise that resolves when save is complete
|
|
pub fn save(&self) -> js_sys::Promise {
|
|
let state = self.export_state();
|
|
let mut storage = storage::IndexedDBStorage::new();
|
|
|
|
future_to_promise(async move {
|
|
storage.init().await?;
|
|
storage.save(&state).await?;
|
|
Ok(JsValue::TRUE)
|
|
})
|
|
}
|
|
|
|
/// Load database from IndexedDB
|
|
/// Returns a Promise<RvLite> with the restored database
|
|
pub fn load(config: RvLiteConfig) -> js_sys::Promise {
|
|
future_to_promise(async move {
|
|
let mut storage = storage::IndexedDBStorage::new();
|
|
storage.init().await?;
|
|
|
|
let state = storage.load().await?;
|
|
|
|
if let Some(state) = state {
|
|
// Create new database with restored state
|
|
let mut rvlite = RvLite::new(config)?;
|
|
rvlite.import_state(&state)?;
|
|
Ok(serde_wasm_bindgen::to_value(&"loaded").unwrap())
|
|
} else {
|
|
Ok(JsValue::NULL)
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Check if saved state exists in IndexedDB
|
|
pub fn has_saved_state() -> js_sys::Promise {
|
|
future_to_promise(async move {
|
|
let mut storage = storage::IndexedDBStorage::new();
|
|
storage.init().await?;
|
|
let exists = storage.exists().await?;
|
|
Ok(JsValue::from_bool(exists))
|
|
})
|
|
}
|
|
|
|
/// Clear saved state from IndexedDB
|
|
pub fn clear_storage() -> js_sys::Promise {
|
|
future_to_promise(async move {
|
|
let mut storage = storage::IndexedDBStorage::new();
|
|
storage.init().await?;
|
|
storage.clear().await?;
|
|
Ok(JsValue::TRUE)
|
|
})
|
|
}
|
|
|
|
/// Export database state as JSON (for manual backup)
|
|
pub fn export_json(&self) -> Result<JsValue, JsValue> {
|
|
let state = self.export_state();
|
|
serde_wasm_bindgen::to_value(&state)
|
|
.map_err(|e| JsValue::from_str(&format!("Export failed: {}", e)))
|
|
}
|
|
|
|
/// Import database state from JSON
|
|
pub fn import_json(&mut self, json: JsValue) -> Result<(), JsValue> {
|
|
let state: RvLiteState = serde_wasm_bindgen::from_value(json)
|
|
.map_err(|e| JsValue::from_str(&format!("Import failed: {}", e)))?;
|
|
self.import_state(&state)
|
|
}
|
|
|
|
// ===== Vector Operations =====
|
|
|
|
/// Insert a vector with optional metadata
|
|
/// Returns the vector ID
|
|
pub fn insert(&self, vector: Vec<f32>, metadata: JsValue) -> Result<String, JsValue> {
|
|
let metadata_map = if metadata.is_null() || metadata.is_undefined() {
|
|
None
|
|
} else {
|
|
Some(
|
|
serde_wasm_bindgen::from_value::<HashMap<String, serde_json::Value>>(metadata)
|
|
.map_err(|e| RvLiteError {
|
|
message: format!("Invalid metadata: {}", e),
|
|
kind: ErrorKind::WasmError,
|
|
})?,
|
|
)
|
|
};
|
|
|
|
let entry = VectorEntry {
|
|
id: None,
|
|
vector,
|
|
metadata: metadata_map,
|
|
};
|
|
|
|
self.db
|
|
.insert(entry)
|
|
.map_err(|e| RvLiteError::from(e).into())
|
|
}
|
|
|
|
/// Insert a vector with a specific ID
|
|
pub fn insert_with_id(
|
|
&self,
|
|
id: String,
|
|
vector: Vec<f32>,
|
|
metadata: JsValue,
|
|
) -> Result<(), JsValue> {
|
|
let metadata_map = if metadata.is_null() || metadata.is_undefined() {
|
|
None
|
|
} else {
|
|
Some(
|
|
serde_wasm_bindgen::from_value::<HashMap<String, serde_json::Value>>(metadata)
|
|
.map_err(|e| RvLiteError {
|
|
message: format!("Invalid metadata: {}", e),
|
|
kind: ErrorKind::WasmError,
|
|
})?,
|
|
)
|
|
};
|
|
|
|
let entry = VectorEntry {
|
|
id: Some(id),
|
|
vector,
|
|
metadata: metadata_map,
|
|
};
|
|
|
|
self.db.insert(entry).map_err(|e| RvLiteError::from(e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Search for similar vectors
|
|
pub fn search(&self, query_vector: Vec<f32>, k: usize) -> Result<JsValue, JsValue> {
|
|
let query = SearchQuery {
|
|
vector: query_vector,
|
|
k,
|
|
filter: None,
|
|
ef_search: None,
|
|
};
|
|
|
|
let results = self.db.search(query).map_err(|e| RvLiteError::from(e))?;
|
|
|
|
serde_wasm_bindgen::to_value(&results).map_err(|e| {
|
|
RvLiteError {
|
|
message: format!("Failed to serialize results: {}", e),
|
|
kind: ErrorKind::WasmError,
|
|
}
|
|
.into()
|
|
})
|
|
}
|
|
|
|
/// Search with metadata filter
|
|
pub fn search_with_filter(
|
|
&self,
|
|
query_vector: Vec<f32>,
|
|
k: usize,
|
|
filter: JsValue,
|
|
) -> Result<JsValue, JsValue> {
|
|
let filter_map = serde_wasm_bindgen::from_value::<HashMap<String, serde_json::Value>>(
|
|
filter,
|
|
)
|
|
.map_err(|e| RvLiteError {
|
|
message: format!("Invalid filter: {}", e),
|
|
kind: ErrorKind::WasmError,
|
|
})?;
|
|
|
|
let query = SearchQuery {
|
|
vector: query_vector,
|
|
k,
|
|
filter: Some(filter_map),
|
|
ef_search: None,
|
|
};
|
|
|
|
let results = self.db.search(query).map_err(|e| RvLiteError::from(e))?;
|
|
|
|
serde_wasm_bindgen::to_value(&results).map_err(|e| {
|
|
RvLiteError {
|
|
message: format!("Failed to serialize results: {}", e),
|
|
kind: ErrorKind::WasmError,
|
|
}
|
|
.into()
|
|
})
|
|
}
|
|
|
|
/// Get a vector by ID
|
|
pub fn get(&self, id: String) -> Result<JsValue, JsValue> {
|
|
let entry = self.db.get(&id).map_err(|e| RvLiteError::from(e))?;
|
|
|
|
serde_wasm_bindgen::to_value(&entry).map_err(|e| {
|
|
RvLiteError {
|
|
message: format!("Failed to serialize entry: {}", e),
|
|
kind: ErrorKind::WasmError,
|
|
}
|
|
.into()
|
|
})
|
|
}
|
|
|
|
/// Delete a vector by ID
|
|
pub fn delete(&self, id: String) -> Result<bool, JsValue> {
|
|
self.db.delete(&id).map_err(|e| RvLiteError::from(e).into())
|
|
}
|
|
|
|
/// Get the number of vectors in the database
|
|
pub fn len(&self) -> Result<usize, JsValue> {
|
|
self.db.len().map_err(|e| RvLiteError::from(e).into())
|
|
}
|
|
|
|
/// Check if database is empty
|
|
pub fn is_empty(&self) -> Result<bool, JsValue> {
|
|
self.db.is_empty().map_err(|e| RvLiteError::from(e).into())
|
|
}
|
|
|
|
/// Get configuration
|
|
pub fn get_config(&self) -> Result<JsValue, JsValue> {
|
|
serde_wasm_bindgen::to_value(&self.config).map_err(|e| {
|
|
RvLiteError {
|
|
message: format!("Failed to serialize config: {}", e),
|
|
kind: ErrorKind::WasmError,
|
|
}
|
|
.into()
|
|
})
|
|
}
|
|
|
|
// ===== SQL Query Methods =====
|
|
|
|
/// Execute SQL query
|
|
///
|
|
/// Supported syntax:
|
|
/// - CREATE TABLE vectors (id TEXT PRIMARY KEY, vector VECTOR(384))
|
|
/// - SELECT * FROM vectors WHERE id = 'x'
|
|
/// - SELECT id, vector <-> '[...]' AS distance FROM vectors ORDER BY distance LIMIT 10
|
|
/// - INSERT INTO vectors (id, vector) VALUES ('x', '[...]')
|
|
/// - DELETE FROM vectors WHERE id = 'x'
|
|
pub fn sql(&self, query: String) -> Result<JsValue, JsValue> {
|
|
// Parse SQL
|
|
let mut parser = sql::SqlParser::new(&query).map_err(|e| RvLiteError {
|
|
message: e.to_string(),
|
|
kind: ErrorKind::SqlError,
|
|
})?;
|
|
let statement = parser.parse().map_err(|e| RvLiteError {
|
|
message: e.to_string(),
|
|
kind: ErrorKind::SqlError,
|
|
})?;
|
|
|
|
// Execute
|
|
let result = self
|
|
.sql_engine
|
|
.execute(statement)
|
|
.map_err(|e| RvLiteError {
|
|
message: e.to_string(),
|
|
kind: ErrorKind::SqlError,
|
|
})?;
|
|
|
|
// Use serde_json + js_sys::JSON::parse for proper serialization
|
|
// (serde_wasm_bindgen can fail silently on complex enum types)
|
|
let json_str = serde_json::to_string(&result).map_err(|e| RvLiteError {
|
|
message: format!("Failed to serialize result: {}", e),
|
|
kind: ErrorKind::WasmError,
|
|
})?;
|
|
|
|
js_sys::JSON::parse(&json_str).map_err(|e| {
|
|
RvLiteError {
|
|
message: format!("Failed to parse JSON: {:?}", e),
|
|
kind: ErrorKind::WasmError,
|
|
}
|
|
.into()
|
|
})
|
|
}
|
|
|
|
// ===== Cypher Query Methods =====
|
|
|
|
/// Execute Cypher query
|
|
///
|
|
/// Supported operations:
|
|
/// - CREATE (n:Label {prop: value})
|
|
/// - MATCH (n:Label) WHERE n.prop = value RETURN n
|
|
/// - CREATE (a)-[r:REL]->(b)
|
|
/// - DELETE n
|
|
pub fn cypher(&mut self, query: String) -> Result<JsValue, JsValue> {
|
|
self.cypher_engine.execute(&query)
|
|
}
|
|
|
|
/// Get Cypher graph statistics
|
|
pub fn cypher_stats(&self) -> Result<JsValue, JsValue> {
|
|
self.cypher_engine.stats()
|
|
}
|
|
|
|
/// Clear the Cypher graph
|
|
pub fn cypher_clear(&mut self) {
|
|
self.cypher_engine.clear();
|
|
}
|
|
|
|
// ===== SPARQL Query Methods =====
|
|
|
|
/// Execute SPARQL query
|
|
///
|
|
/// Supported operations:
|
|
/// - SELECT ?s ?p ?o WHERE { ?s ?p ?o }
|
|
/// - SELECT ?s WHERE { ?s <predicate> ?o }
|
|
/// - ASK { ?s ?p ?o }
|
|
pub fn sparql(&self, query: String) -> Result<JsValue, JsValue> {
|
|
let parsed = sparql::parse_sparql(&query).map_err(|e| RvLiteError {
|
|
message: format!("SPARQL parse error: {}", e),
|
|
kind: ErrorKind::SparqlError,
|
|
})?;
|
|
|
|
let result = sparql::execute_sparql(&self.triple_store, &parsed)
|
|
.map_err(|e| RvLiteError::from(e))?;
|
|
|
|
// Convert result to serializable format
|
|
let serializable = convert_sparql_result(&result);
|
|
|
|
// Convert JSON to string and then parse in JS for proper object conversion
|
|
let json_string = serializable.to_string();
|
|
let js_obj = js_sys::JSON::parse(&json_string).map_err(|e| RvLiteError {
|
|
message: format!("Failed to parse JSON: {:?}", e),
|
|
kind: ErrorKind::WasmError,
|
|
})?;
|
|
|
|
Ok(js_obj)
|
|
}
|
|
|
|
/// Add an RDF triple
|
|
///
|
|
/// # Arguments
|
|
/// * `subject` - Subject IRI or blank node (e.g., "<http://example.org/s>" or "_:b1")
|
|
/// * `predicate` - Predicate IRI (e.g., "<http://example.org/p>")
|
|
/// * `object` - Object IRI, blank node, or literal (e.g., "<http://example.org/o>" or '"value"')
|
|
pub fn add_triple(
|
|
&self,
|
|
subject: String,
|
|
predicate: String,
|
|
object: String,
|
|
) -> Result<(), JsValue> {
|
|
let subj = parse_rdf_term(&subject)?;
|
|
let pred = parse_iri(&predicate)?;
|
|
let obj = parse_rdf_term(&object)?;
|
|
|
|
let triple = sparql::Triple::new(subj, pred, obj);
|
|
self.triple_store.insert(triple);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the number of triples in the store
|
|
pub fn triple_count(&self) -> usize {
|
|
self.triple_store.count()
|
|
}
|
|
|
|
/// Clear all triples
|
|
pub fn clear_triples(&self) {
|
|
self.triple_store.clear();
|
|
}
|
|
}
|
|
|
|
// Private impl block for state export/import
|
|
impl RvLite {
|
|
/// Export the complete database state
|
|
fn export_state(&self) -> RvLiteState {
|
|
use storage::state::*;
|
|
|
|
// Get current timestamp
|
|
let saved_at = js_sys::Date::now() as u64;
|
|
|
|
// Export vector state
|
|
let vector_entries = self
|
|
.db
|
|
.keys()
|
|
.unwrap_or_default()
|
|
.iter()
|
|
.filter_map(|id| {
|
|
self.db
|
|
.get(id)
|
|
.ok()
|
|
.flatten()
|
|
.map(|entry| storage::state::VectorEntry {
|
|
id: entry.id.unwrap_or_default(),
|
|
vector: entry.vector,
|
|
metadata: entry.metadata,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let vectors = VectorState {
|
|
entries: vector_entries,
|
|
dimensions: self.config.dimensions,
|
|
distance_metric: self.config.distance_metric.clone(),
|
|
next_id: 0, // Will be recalculated on load
|
|
};
|
|
|
|
// Export graph state
|
|
let graph = self.cypher_engine.export_state();
|
|
|
|
// Export triple store state
|
|
let triples = self.export_triple_state();
|
|
|
|
// Export SQL schemas (not fully implemented yet)
|
|
let sql_schemas = Vec::new();
|
|
|
|
RvLiteState {
|
|
version: 1,
|
|
saved_at,
|
|
vectors,
|
|
graph,
|
|
triples,
|
|
sql_schemas,
|
|
}
|
|
}
|
|
|
|
/// Import state into the database
|
|
fn import_state(&mut self, state: &RvLiteState) -> Result<(), JsValue> {
|
|
// Import vectors
|
|
for entry in &state.vectors.entries {
|
|
let vector_entry = VectorEntry {
|
|
id: Some(entry.id.clone()),
|
|
vector: entry.vector.clone(),
|
|
metadata: entry.metadata.clone(),
|
|
};
|
|
self.db
|
|
.insert(vector_entry)
|
|
.map_err(|e| RvLiteError::from(e))?;
|
|
}
|
|
|
|
// Import graph
|
|
self.cypher_engine.import_state(&state.graph)?;
|
|
|
|
// Import triples
|
|
self.import_triple_state(&state.triples)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Export triple store state
|
|
fn export_triple_state(&self) -> storage::state::TripleStoreState {
|
|
use storage::state::*;
|
|
|
|
let triples: Vec<TripleState> = self
|
|
.triple_store
|
|
.all_triples()
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(id, t)| TripleState {
|
|
id: id as u64,
|
|
subject: rdf_term_to_state(&t.subject),
|
|
predicate: t.predicate.0.clone(),
|
|
object: rdf_term_to_state(&t.object),
|
|
})
|
|
.collect();
|
|
|
|
TripleStoreState {
|
|
triples,
|
|
named_graphs: HashMap::new(),
|
|
default_graph: Vec::new(),
|
|
next_id: 0,
|
|
}
|
|
}
|
|
|
|
/// Import triple store state
|
|
fn import_triple_state(&self, state: &storage::state::TripleStoreState) -> Result<(), JsValue> {
|
|
self.triple_store.clear();
|
|
|
|
for triple_state in &state.triples {
|
|
let subject = state_to_rdf_term(&triple_state.subject)?;
|
|
let predicate = sparql::Iri::new(&triple_state.predicate);
|
|
let object = state_to_rdf_term(&triple_state.object)?;
|
|
|
|
let triple = sparql::Triple::new(subject, predicate, object);
|
|
self.triple_store.insert(triple);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// Helper function to convert RdfTerm to clean JSON value
|
|
fn term_to_json(term: &sparql::ast::RdfTerm) -> serde_json::Value {
|
|
use sparql::ast::RdfTerm;
|
|
match term {
|
|
RdfTerm::Iri(iri) => serde_json::json!({
|
|
"type": "iri",
|
|
"value": iri.as_str()
|
|
}),
|
|
RdfTerm::Literal(lit) => {
|
|
let mut obj = serde_json::Map::new();
|
|
obj.insert("type".to_string(), serde_json::json!("literal"));
|
|
obj.insert("value".to_string(), serde_json::json!(lit.value.clone()));
|
|
if let Some(lang) = &lit.language {
|
|
obj.insert("language".to_string(), serde_json::json!(lang));
|
|
}
|
|
obj.insert(
|
|
"datatype".to_string(),
|
|
serde_json::json!(lit.datatype.as_str()),
|
|
);
|
|
serde_json::Value::Object(obj)
|
|
}
|
|
RdfTerm::BlankNode(id) => serde_json::json!({
|
|
"type": "bnode",
|
|
"value": id
|
|
}),
|
|
}
|
|
}
|
|
|
|
// Helper function to convert SPARQL result to serializable format
|
|
fn convert_sparql_result(result: &sparql::executor::QueryResult) -> serde_json::Value {
|
|
use sparql::executor::QueryResult;
|
|
|
|
match result {
|
|
QueryResult::Select(select_result) => {
|
|
let bindings: Vec<serde_json::Value> = select_result
|
|
.bindings
|
|
.iter()
|
|
.map(|binding| {
|
|
let mut obj = serde_json::Map::new();
|
|
for (var, term) in binding {
|
|
obj.insert(var.clone(), term_to_json(term));
|
|
}
|
|
serde_json::Value::Object(obj)
|
|
})
|
|
.collect();
|
|
|
|
serde_json::json!({
|
|
"type": "select",
|
|
"variables": select_result.variables,
|
|
"bindings": bindings
|
|
})
|
|
}
|
|
QueryResult::Ask(result) => {
|
|
serde_json::json!({
|
|
"type": "ask",
|
|
"result": result
|
|
})
|
|
}
|
|
QueryResult::Construct(triples) => {
|
|
let triple_json: Vec<serde_json::Value> = triples
|
|
.iter()
|
|
.map(|t| {
|
|
serde_json::json!({
|
|
"subject": term_to_json(&t.subject),
|
|
"predicate": t.predicate.0.clone(),
|
|
"object": term_to_json(&t.object)
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
serde_json::json!({
|
|
"type": "construct",
|
|
"triples": triple_json
|
|
})
|
|
}
|
|
QueryResult::Describe(triples) => {
|
|
let triple_json: Vec<serde_json::Value> = triples
|
|
.iter()
|
|
.map(|t| {
|
|
serde_json::json!({
|
|
"subject": term_to_json(&t.subject),
|
|
"predicate": t.predicate.0.clone(),
|
|
"object": term_to_json(&t.object)
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
serde_json::json!({
|
|
"type": "describe",
|
|
"triples": triple_json
|
|
})
|
|
}
|
|
QueryResult::Update => {
|
|
serde_json::json!({
|
|
"type": "update",
|
|
"success": true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions for parsing RDF terms
|
|
fn parse_rdf_term(s: &str) -> Result<sparql::RdfTerm, JsValue> {
|
|
let s = s.trim();
|
|
if s.starts_with('<') && s.ends_with('>') {
|
|
Ok(sparql::RdfTerm::iri(&s[1..s.len() - 1]))
|
|
} else if s.starts_with("_:") {
|
|
Ok(sparql::RdfTerm::blank(&s[2..]))
|
|
} else if s.starts_with('"') {
|
|
let end = s.rfind('"').unwrap_or(s.len() - 1);
|
|
let value = &s[1..end];
|
|
Ok(sparql::RdfTerm::literal(value))
|
|
} else {
|
|
Ok(sparql::RdfTerm::literal(s))
|
|
}
|
|
}
|
|
|
|
fn parse_iri(s: &str) -> Result<sparql::Iri, JsValue> {
|
|
let s = s.trim();
|
|
if s.starts_with('<') && s.ends_with('>') {
|
|
Ok(sparql::Iri::new(&s[1..s.len() - 1]))
|
|
} else {
|
|
Ok(sparql::Iri::new(s))
|
|
}
|
|
}
|
|
|
|
// Helper functions for RDF term state conversion
|
|
fn rdf_term_to_state(term: &sparql::RdfTerm) -> storage::state::RdfTermState {
|
|
use storage::state::RdfTermState;
|
|
|
|
match term {
|
|
sparql::RdfTerm::Iri(iri) => RdfTermState::Iri {
|
|
value: iri.0.clone(),
|
|
},
|
|
sparql::RdfTerm::Literal(lit) => RdfTermState::Literal {
|
|
value: lit.value.clone(),
|
|
datatype: lit.datatype.0.clone(),
|
|
language: lit.language.clone(),
|
|
},
|
|
sparql::RdfTerm::BlankNode(id) => RdfTermState::BlankNode { id: id.clone() },
|
|
}
|
|
}
|
|
|
|
fn state_to_rdf_term(state: &storage::state::RdfTermState) -> Result<sparql::RdfTerm, JsValue> {
|
|
use storage::state::RdfTermState;
|
|
|
|
match state {
|
|
RdfTermState::Iri { value } => Ok(sparql::RdfTerm::iri(value)),
|
|
RdfTermState::Literal {
|
|
value,
|
|
datatype: _,
|
|
language: _,
|
|
} => Ok(sparql::RdfTerm::literal(value)),
|
|
RdfTermState::BlankNode { id } => Ok(sparql::RdfTerm::blank(id)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_config_creation() {
|
|
let config = RvLiteConfig::new(384);
|
|
assert_eq!(config.dimensions, 384);
|
|
assert_eq!(config.distance_metric, "cosine");
|
|
}
|
|
}
|