use crate::error::{MemoryError, Result};
use crate::persistence::Persistence;
use crate::semantic_bridge::{CanonicalConcept, ConceptGraph};
use libsql::params;
impl Persistence {
pub async fn save_canonical_concept(&self, concept: &CanonicalConcept) -> Result<()> {
let _permit = self.acquire_remote_slot().await?;
let conn = self.connect().await?;
let labels_json = serde_json::to_string(&concept.labels)?;
let related_json = serde_json::to_string(&concept.related)?;
conn.execute(
"INSERT OR REPLACE INTO csm_canonical (id, version, labels_json, related_json)
VALUES (?1, ?2, ?3, ?4)",
params![
concept.id.clone(),
concept.version as i64,
labels_json,
related_json
],
)
.await
.map_err(|e| MemoryError::database(format!("Failed to save canonical concept: {}", e)))?;
Ok(())
}
pub async fn delete_canonical_concept(&self, id: &str) -> Result<()> {
let _permit = self.acquire_remote_slot().await?;
let conn = self.connect().await?;
conn.execute("DELETE FROM csm_canonical WHERE id = ?1", params![id])
.await
.map_err(|e| {
MemoryError::database(format!("Failed to delete canonical concept: {}", e))
})?;
Ok(())
}
pub async fn load_canonical_concept(&self, id: &str) -> Result<Option<CanonicalConcept>> {
let _permit = self.acquire_remote_slot().await?;
let conn = self.connect().await?;
let mut rows = conn
.query(
"SELECT id, version, labels_json, related_json FROM csm_canonical WHERE id = ?1",
params![id],
)
.await
.map_err(|e| {
MemoryError::database(format!("Failed to load canonical concept: {}", e))
})?;
if let Some(row) = rows.next().await.map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept row: {}", e))
})? {
let id: String = row.get(0).map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept id: {}", e))
})?;
let version: i64 = row.get(1).map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept version: {}", e))
})?;
let labels_json: String = row.get(2).map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept labels: {}", e))
})?;
let related_json: String = row.get(3).map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept related: {}", e))
})?;
let labels: Vec<String> = serde_json::from_str(&labels_json)?;
let related: Vec<String> = serde_json::from_str(&related_json)?;
Ok(Some(CanonicalConcept {
id,
version: version as u32,
labels,
related,
}))
} else {
Ok(None)
}
}
pub async fn load_all_canonical_concepts(&self) -> Result<Vec<CanonicalConcept>> {
let _permit = self.acquire_remote_slot().await?;
let conn = self.connect().await?;
let mut rows = conn
.query(
"SELECT id, version, labels_json, related_json FROM csm_canonical",
params![],
)
.await
.map_err(|e| {
MemoryError::database(format!("Failed to load canonical concepts: {}", e))
})?;
let mut concepts = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept row: {}", e))
})? {
let id: String = row.get(0).map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept id: {}", e))
})?;
let version: i64 = row.get(1).map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept version: {}", e))
})?;
let labels_json: String = row.get(2).map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept labels: {}", e))
})?;
let related_json: String = row.get(3).map_err(|e| {
MemoryError::database(format!("Failed to read canonical concept related: {}", e))
})?;
let labels: Vec<String> = serde_json::from_str(&labels_json)?;
let related: Vec<String> = serde_json::from_str(&related_json)?;
concepts.push(CanonicalConcept {
id,
version: version as u32,
labels,
related,
});
}
Ok(concepts)
}
pub async fn save_concept_graph(&self, graph: &ConceptGraph) -> Result<()> {
let _permit = self.acquire_remote_slot().await?;
let conn = self.connect().await?;
conn.execute("BEGIN", ())
.await
.map_err(|e| MemoryError::database(format!("Failed to begin transaction: {}", e)))?;
if let Err(e) = conn.execute("DELETE FROM csm_canonical", params![]).await {
let _ = conn.execute("ROLLBACK", ()).await;
return Err(MemoryError::database(format!(
"Failed to clear canonical concepts: {}",
e
)));
}
let mut first_error: Option<MemoryError> = None;
for concept in graph.all_concepts() {
let labels_json = match serde_json::to_string(&concept.labels) {
Ok(j) => j,
Err(e) => {
first_error = Some(MemoryError::Serialization(e));
break;
}
};
let related_json = match serde_json::to_string(&concept.related) {
Ok(j) => j,
Err(e) => {
first_error = Some(MemoryError::Serialization(e));
break;
}
};
if let Err(e) = conn
.execute(
"INSERT INTO csm_canonical (id, version, labels_json, related_json)
VALUES (?1, ?2, ?3, ?4)",
params![
concept.id.clone(),
concept.version as i64,
labels_json,
related_json
],
)
.await
{
first_error = Some(MemoryError::database(format!(
"Failed to save canonical concept: {}",
e
)));
break;
}
}
if let Some(error) = first_error {
let _ = conn.execute("ROLLBACK", ()).await;
return Err(error);
}
conn.execute("COMMIT", ())
.await
.map_err(|e| MemoryError::database(format!("Failed to commit transaction: {}", e)))?;
Ok(())
}
pub async fn load_concept_graph(&self) -> Result<ConceptGraph> {
let concepts = self.load_all_canonical_concepts().await?;
let mut graph = ConceptGraph::new();
for concept in concepts {
graph.add_concept(concept);
}
Ok(graph)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::semantic_bridge::CanonicalConcept;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_save_and_load_canonical_concept() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path().to_str().unwrap();
let persistence = Persistence::new_local(path).await.unwrap();
let concept = CanonicalConcept::new("test-concept")
.with_label("label1")
.with_label("label2")
.with_related("related-concept");
persistence.save_canonical_concept(&concept).await.unwrap();
let loaded = persistence
.load_canonical_concept("test-concept")
.await
.unwrap();
assert!(loaded.is_some());
let loaded = loaded.unwrap();
assert_eq!(loaded.id, "test-concept");
assert_eq!(loaded.labels, vec!["label1", "label2"]);
assert_eq!(loaded.related, vec!["related-concept"]);
}
#[tokio::test]
async fn test_delete_canonical_concept() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path().to_str().unwrap();
let persistence = Persistence::new_local(path).await.unwrap();
let concept = CanonicalConcept::new("to-delete");
persistence.save_canonical_concept(&concept).await.unwrap();
persistence
.delete_canonical_concept("to-delete")
.await
.unwrap();
let loaded = persistence
.load_canonical_concept("to-delete")
.await
.unwrap();
assert!(loaded.is_none());
}
#[tokio::test]
async fn test_save_and_load_concept_graph() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path().to_str().unwrap();
let persistence = Persistence::new_local(path).await.unwrap();
let mut graph = ConceptGraph::new();
graph.add_concept(
CanonicalConcept::new("c1")
.with_label("label1")
.with_related("c2"),
);
graph.add_concept(CanonicalConcept::new("c2").with_label("label2"));
persistence.save_concept_graph(&graph).await.unwrap();
let loaded = persistence.load_concept_graph().await.unwrap();
assert_eq!(loaded.concept_count(), 2);
assert_eq!(loaded.label_count(), 2);
}
}