1use crate::embedder::f32_to_bytes;
8use crate::errors::AppError;
9use crate::storage::utils::with_busy_retry;
10use rusqlite::{params, Connection};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
18#[serde(deny_unknown_fields)]
19pub struct NewEntity {
20 pub name: String,
21 #[serde(alias = "type")]
22 pub entity_type: String,
23 pub description: Option<String>,
24}
25
26#[derive(Debug, Serialize, Deserialize, Clone)]
31#[serde(deny_unknown_fields)]
32pub struct NewRelationship {
33 #[serde(alias = "from")]
34 pub source: String,
35 #[serde(alias = "to")]
36 pub target: String,
37 pub relation: String,
38 pub strength: f64,
39 pub description: Option<String>,
40}
41
42pub fn upsert_entity(conn: &Connection, namespace: &str, e: &NewEntity) -> Result<i64, AppError> {
51 conn.execute(
52 "INSERT INTO entities (namespace, name, type, description)
53 VALUES (?1, ?2, ?3, ?4)
54 ON CONFLICT(namespace, name) DO UPDATE SET
55 type = excluded.type,
56 description = COALESCE(excluded.description, entities.description),
57 updated_at = unixepoch()",
58 params![namespace, e.name, e.entity_type, e.description],
59 )?;
60 let id: i64 = conn.query_row(
61 "SELECT id FROM entities WHERE namespace = ?1 AND name = ?2",
62 params![namespace, e.name],
63 |r| r.get(0),
64 )?;
65 Ok(id)
66}
67
68pub fn upsert_entity_vec(
80 conn: &Connection,
81 entity_id: i64,
82 namespace: &str,
83 entity_type: &str,
84 embedding: &[f32],
85 name: &str,
86) -> Result<(), AppError> {
87 let embedding_bytes = f32_to_bytes(embedding);
90 with_busy_retry(|| {
91 conn.execute(
92 "DELETE FROM vec_entities WHERE entity_id = ?1",
93 params![entity_id],
94 )?;
95 conn.execute(
96 "INSERT INTO vec_entities(entity_id, namespace, type, embedding, name)
97 VALUES (?1, ?2, ?3, ?4, ?5)",
98 params![entity_id, namespace, entity_type, &embedding_bytes, name],
99 )?;
100 Ok(())
101 })
102}
103
104pub fn upsert_relationship(
113 conn: &Connection,
114 namespace: &str,
115 source_id: i64,
116 target_id: i64,
117 rel: &NewRelationship,
118) -> Result<i64, AppError> {
119 conn.execute(
120 "INSERT INTO relationships (namespace, source_id, target_id, relation, weight, description)
121 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
122 ON CONFLICT(source_id, target_id, relation) DO UPDATE SET
123 weight = excluded.weight,
124 description = COALESCE(excluded.description, relationships.description)",
125 params![
126 namespace,
127 source_id,
128 target_id,
129 rel.relation,
130 rel.strength,
131 rel.description
132 ],
133 )?;
134 let id: i64 = conn.query_row(
135 "SELECT id FROM relationships WHERE source_id=?1 AND target_id=?2 AND relation=?3",
136 params![source_id, target_id, rel.relation],
137 |r| r.get(0),
138 )?;
139 Ok(id)
140}
141
142pub fn link_memory_entity(
143 conn: &Connection,
144 memory_id: i64,
145 entity_id: i64,
146) -> Result<(), AppError> {
147 conn.execute(
148 "INSERT OR IGNORE INTO memory_entities (memory_id, entity_id) VALUES (?1, ?2)",
149 params![memory_id, entity_id],
150 )?;
151 Ok(())
152}
153
154pub fn link_memory_relationship(
155 conn: &Connection,
156 memory_id: i64,
157 rel_id: i64,
158) -> Result<(), AppError> {
159 conn.execute(
160 "INSERT OR IGNORE INTO memory_relationships (memory_id, relationship_id) VALUES (?1, ?2)",
161 params![memory_id, rel_id],
162 )?;
163 Ok(())
164}
165
166pub fn increment_degree(conn: &Connection, entity_id: i64) -> Result<(), AppError> {
167 conn.execute(
168 "UPDATE entities SET degree = degree + 1 WHERE id = ?1",
169 params![entity_id],
170 )?;
171 Ok(())
172}
173
174pub fn find_entity_id(
176 conn: &Connection,
177 namespace: &str,
178 name: &str,
179) -> Result<Option<i64>, AppError> {
180 let mut stmt =
181 conn.prepare_cached("SELECT id FROM entities WHERE namespace = ?1 AND name = ?2")?;
182 match stmt.query_row(params![namespace, name], |r| r.get::<_, i64>(0)) {
183 Ok(id) => Ok(Some(id)),
184 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
185 Err(e) => Err(AppError::Database(e)),
186 }
187}
188
189#[derive(Debug, Serialize)]
191pub struct RelationshipRow {
192 pub id: i64,
193 pub namespace: String,
194 pub source_id: i64,
195 pub target_id: i64,
196 pub relation: String,
197 pub weight: f64,
198 pub description: Option<String>,
199}
200
201pub fn find_relationship(
203 conn: &Connection,
204 source_id: i64,
205 target_id: i64,
206 relation: &str,
207) -> Result<Option<RelationshipRow>, AppError> {
208 let mut stmt = conn.prepare_cached(
209 "SELECT id, namespace, source_id, target_id, relation, weight, description
210 FROM relationships
211 WHERE source_id = ?1 AND target_id = ?2 AND relation = ?3",
212 )?;
213 match stmt.query_row(params![source_id, target_id, relation], |r| {
214 Ok(RelationshipRow {
215 id: r.get(0)?,
216 namespace: r.get(1)?,
217 source_id: r.get(2)?,
218 target_id: r.get(3)?,
219 relation: r.get(4)?,
220 weight: r.get(5)?,
221 description: r.get(6)?,
222 })
223 }) {
224 Ok(row) => Ok(Some(row)),
225 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
226 Err(e) => Err(AppError::Database(e)),
227 }
228}
229
230pub fn create_or_fetch_relationship(
233 conn: &Connection,
234 namespace: &str,
235 source_id: i64,
236 target_id: i64,
237 relation: &str,
238 weight: f64,
239 description: Option<&str>,
240) -> Result<(i64, bool), AppError> {
241 let existing = find_relationship(conn, source_id, target_id, relation)?;
243 if let Some(row) = existing {
244 return Ok((row.id, false));
245 }
246 conn.execute(
247 "INSERT INTO relationships (namespace, source_id, target_id, relation, weight, description)
248 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
249 params![
250 namespace,
251 source_id,
252 target_id,
253 relation,
254 weight,
255 description
256 ],
257 )?;
258 let id: i64 = conn.query_row(
259 "SELECT id FROM relationships WHERE source_id = ?1 AND target_id = ?2 AND relation = ?3",
260 params![source_id, target_id, relation],
261 |r| r.get(0),
262 )?;
263 Ok((id, true))
264}
265
266pub fn delete_relationship_by_id(conn: &Connection, relationship_id: i64) -> Result<(), AppError> {
268 conn.execute(
269 "DELETE FROM memory_relationships WHERE relationship_id = ?1",
270 params![relationship_id],
271 )?;
272 conn.execute(
273 "DELETE FROM relationships WHERE id = ?1",
274 params![relationship_id],
275 )?;
276 Ok(())
277}
278
279pub fn recalculate_degree(conn: &Connection, entity_id: i64) -> Result<(), AppError> {
281 conn.execute(
282 "UPDATE entities
283 SET degree = (SELECT COUNT(*) FROM relationships
284 WHERE source_id = entities.id OR target_id = entities.id)
285 WHERE id = ?1",
286 params![entity_id],
287 )?;
288 Ok(())
289}
290
291#[derive(Debug, Serialize, Clone)]
293pub struct EntityNode {
294 pub id: i64,
295 pub name: String,
296 pub namespace: String,
297 pub kind: String,
298}
299
300pub fn list_entities(
302 conn: &Connection,
303 namespace: Option<&str>,
304) -> Result<Vec<EntityNode>, AppError> {
305 if let Some(ns) = namespace {
306 let mut stmt = conn.prepare(
307 "SELECT id, name, namespace, type FROM entities WHERE namespace = ?1 ORDER BY id",
308 )?;
309 let rows = stmt
310 .query_map(params![ns], |r| {
311 Ok(EntityNode {
312 id: r.get(0)?,
313 name: r.get(1)?,
314 namespace: r.get(2)?,
315 kind: r.get(3)?,
316 })
317 })?
318 .collect::<Result<Vec<_>, _>>()?;
319 Ok(rows)
320 } else {
321 let mut stmt =
322 conn.prepare("SELECT id, name, namespace, type FROM entities ORDER BY namespace, id")?;
323 let rows = stmt
324 .query_map([], |r| {
325 Ok(EntityNode {
326 id: r.get(0)?,
327 name: r.get(1)?,
328 namespace: r.get(2)?,
329 kind: r.get(3)?,
330 })
331 })?
332 .collect::<Result<Vec<_>, _>>()?;
333 Ok(rows)
334 }
335}
336
337pub fn list_relationships_by_namespace(
339 conn: &Connection,
340 namespace: Option<&str>,
341) -> Result<Vec<RelationshipRow>, AppError> {
342 if let Some(ns) = namespace {
343 let mut stmt = conn.prepare(
344 "SELECT r.id, r.namespace, r.source_id, r.target_id, r.relation, r.weight, r.description
345 FROM relationships r
346 JOIN entities se ON se.id = r.source_id AND se.namespace = ?1
347 JOIN entities te ON te.id = r.target_id AND te.namespace = ?1
348 ORDER BY r.id",
349 )?;
350 let rows = stmt
351 .query_map(params![ns], |r| {
352 Ok(RelationshipRow {
353 id: r.get(0)?,
354 namespace: r.get(1)?,
355 source_id: r.get(2)?,
356 target_id: r.get(3)?,
357 relation: r.get(4)?,
358 weight: r.get(5)?,
359 description: r.get(6)?,
360 })
361 })?
362 .collect::<Result<Vec<_>, _>>()?;
363 Ok(rows)
364 } else {
365 let mut stmt = conn.prepare(
366 "SELECT id, namespace, source_id, target_id, relation, weight, description
367 FROM relationships ORDER BY id",
368 )?;
369 let rows = stmt
370 .query_map([], |r| {
371 Ok(RelationshipRow {
372 id: r.get(0)?,
373 namespace: r.get(1)?,
374 source_id: r.get(2)?,
375 target_id: r.get(3)?,
376 relation: r.get(4)?,
377 weight: r.get(5)?,
378 description: r.get(6)?,
379 })
380 })?
381 .collect::<Result<Vec<_>, _>>()?;
382 Ok(rows)
383 }
384}
385
386pub fn find_orphan_entity_ids(
388 conn: &Connection,
389 namespace: Option<&str>,
390) -> Result<Vec<i64>, AppError> {
391 if let Some(ns) = namespace {
392 let mut stmt = conn.prepare(
393 "SELECT e.id FROM entities e
394 WHERE e.namespace = ?1
395 AND NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
396 AND NOT EXISTS (
397 SELECT 1 FROM relationships r
398 WHERE r.source_id = e.id OR r.target_id = e.id
399 )",
400 )?;
401 let ids = stmt
402 .query_map(params![ns], |r| r.get::<_, i64>(0))?
403 .collect::<Result<Vec<_>, _>>()?;
404 Ok(ids)
405 } else {
406 let mut stmt = conn.prepare(
407 "SELECT e.id FROM entities e
408 WHERE NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
409 AND NOT EXISTS (
410 SELECT 1 FROM relationships r
411 WHERE r.source_id = e.id OR r.target_id = e.id
412 )",
413 )?;
414 let ids = stmt
415 .query_map([], |r| r.get::<_, i64>(0))?
416 .collect::<Result<Vec<_>, _>>()?;
417 Ok(ids)
418 }
419}
420
421pub fn delete_entities_by_ids(conn: &Connection, entity_ids: &[i64]) -> Result<usize, AppError> {
423 if entity_ids.is_empty() {
424 return Ok(0);
425 }
426 let mut removed = 0usize;
427 for id in entity_ids {
428 let _ = conn.execute("DELETE FROM vec_entities WHERE entity_id = ?1", params![id]);
430 let affected = conn.execute("DELETE FROM entities WHERE id = ?1", params![id])?;
431 removed += affected;
432 }
433 Ok(removed)
434}
435
436pub fn knn_search(
437 conn: &Connection,
438 embedding: &[f32],
439 namespace: &str,
440 k: usize,
441) -> Result<Vec<(i64, f32)>, AppError> {
442 let bytes = f32_to_bytes(embedding);
443 let mut stmt = conn.prepare(
444 "SELECT entity_id, distance FROM vec_entities
445 WHERE embedding MATCH ?1 AND namespace = ?2
446 ORDER BY distance LIMIT ?3",
447 )?;
448 let rows = stmt
449 .query_map(params![bytes, namespace, k as i64], |r| {
450 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
451 })?
452 .collect::<Result<Vec<_>, _>>()?;
453 Ok(rows)
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use crate::constants::EMBEDDING_DIM;
460 use crate::storage::connection::register_vec_extension;
461 use rusqlite::Connection;
462 use tempfile::TempDir;
463
464 type TestResult = Result<(), Box<dyn std::error::Error>>;
465
466 fn setup_db() -> Result<(TempDir, Connection), Box<dyn std::error::Error>> {
467 register_vec_extension();
468 let tmp = TempDir::new()?;
469 let db_path = tmp.path().join("test.db");
470 let mut conn = Connection::open(&db_path)?;
471 crate::migrations::runner().run(&mut conn)?;
472 Ok((tmp, conn))
473 }
474
475 fn insert_memory(conn: &Connection) -> Result<i64, Box<dyn std::error::Error>> {
476 conn.execute(
477 "INSERT INTO memories (namespace, name, type, description, body, body_hash)
478 VALUES ('global', 'test-mem', 'user', 'desc', 'body', 'hash1')",
479 [],
480 )?;
481 Ok(conn.last_insert_rowid())
482 }
483
484 fn new_entity_helper(name: &str) -> NewEntity {
485 NewEntity {
486 name: name.to_string(),
487 entity_type: "project".to_string(),
488 description: None,
489 }
490 }
491
492 fn embedding_zero() -> Vec<f32> {
493 vec![0.0f32; EMBEDDING_DIM]
494 }
495
496 #[test]
501 fn test_upsert_entity_creates_new() -> TestResult {
502 let (_tmp, conn) = setup_db()?;
503 let e = new_entity_helper("projeto-alpha");
504 let id = upsert_entity(&conn, "global", &e)?;
505 assert!(id > 0);
506 Ok(())
507 }
508
509 #[test]
510 fn test_upsert_entity_idempotent_returns_same_id() -> TestResult {
511 let (_tmp, conn) = setup_db()?;
512 let e = new_entity_helper("projeto-beta");
513 let id1 = upsert_entity(&conn, "global", &e)?;
514 let id2 = upsert_entity(&conn, "global", &e)?;
515 assert_eq!(id1, id2);
516 Ok(())
517 }
518
519 #[test]
520 fn test_upsert_entity_updates_description() -> TestResult {
521 let (_tmp, conn) = setup_db()?;
522 let e1 = new_entity_helper("projeto-gamma");
523 let id1 = upsert_entity(&conn, "global", &e1)?;
524
525 let e2 = NewEntity {
526 name: "projeto-gamma".to_string(),
527 entity_type: "tool".to_string(),
528 description: Some("nova desc".to_string()),
529 };
530 let id2 = upsert_entity(&conn, "global", &e2)?;
531 assert_eq!(id1, id2);
532
533 let desc: Option<String> = conn.query_row(
534 "SELECT description FROM entities WHERE id = ?1",
535 params![id1],
536 |r| r.get(0),
537 )?;
538 assert_eq!(desc.as_deref(), Some("nova desc"));
539 Ok(())
540 }
541
542 #[test]
543 fn test_upsert_entity_different_namespaces_create_distinct_records() -> TestResult {
544 let (_tmp, conn) = setup_db()?;
545 let e = new_entity_helper("compartilhada");
546 let id1 = upsert_entity(&conn, "ns1", &e)?;
547 let id2 = upsert_entity(&conn, "ns2", &e)?;
548 assert_ne!(id1, id2);
549 Ok(())
550 }
551
552 #[test]
557 fn test_upsert_entity_vec_first_time_without_conflict() -> TestResult {
558 let (_tmp, conn) = setup_db()?;
559 let e = new_entity_helper("vec-nova");
560 let entity_id = upsert_entity(&conn, "global", &e)?;
561 let emb = embedding_zero();
562
563 let result = upsert_entity_vec(&conn, entity_id, "global", "project", &emb, "vec-nova");
564 assert!(result.is_ok(), "first insertion must succeed");
565
566 let count: i64 = conn.query_row(
567 "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
568 params![entity_id],
569 |r| r.get(0),
570 )?;
571 assert_eq!(count, 1, "must have exactly one row after insertion");
572 Ok(())
573 }
574
575 #[test]
576 fn test_upsert_entity_vec_second_time_replaces_without_error() -> TestResult {
577 let (_tmp, conn) = setup_db()?;
579 let e = new_entity_helper("vec-existente");
580 let entity_id = upsert_entity(&conn, "global", &e)?;
581 let emb = embedding_zero();
582
583 upsert_entity_vec(&conn, entity_id, "global", "project", &emb, "vec-existente")?;
584
585 let result = upsert_entity_vec(&conn, entity_id, "global", "tool", &emb, "vec-existente");
587 assert!(
588 result.is_ok(),
589 "second insertion (replace) must succeed: {result:?}"
590 );
591
592 let count: i64 = conn.query_row(
593 "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
594 params![entity_id],
595 |r| r.get(0),
596 )?;
597 assert_eq!(count, 1, "must have exactly one row after replacement");
598 Ok(())
599 }
600
601 #[test]
602 fn test_upsert_entity_vec_multiple_independent_entities() -> TestResult {
603 let (_tmp, conn) = setup_db()?;
604 let emb = embedding_zero();
605
606 for i in 0..3i64 {
607 let nome = format!("ent-{i}");
608 let e = new_entity_helper(&nome);
609 let entity_id = upsert_entity(&conn, "global", &e)?;
610 upsert_entity_vec(&conn, entity_id, "global", "project", &emb, &nome)?;
611 }
612
613 let count: i64 = conn.query_row("SELECT COUNT(*) FROM vec_entities", [], |r| r.get(0))?;
614 assert_eq!(count, 3, "must have three distinct rows in vec_entities");
615 Ok(())
616 }
617
618 #[test]
623 fn test_find_entity_id_existing_returns_some() -> TestResult {
624 let (_tmp, conn) = setup_db()?;
625 let e = new_entity_helper("entidade-busca");
626 let id_inserido = upsert_entity(&conn, "global", &e)?;
627 let id_encontrado = find_entity_id(&conn, "global", "entidade-busca")?;
628 assert_eq!(id_encontrado, Some(id_inserido));
629 Ok(())
630 }
631
632 #[test]
633 fn test_find_entity_id_missing_returns_none() -> TestResult {
634 let (_tmp, conn) = setup_db()?;
635 let id = find_entity_id(&conn, "global", "nao-existe")?;
636 assert_eq!(id, None);
637 Ok(())
638 }
639
640 #[test]
645 fn test_delete_entities_by_ids_empty_list_returns_zero() -> TestResult {
646 let (_tmp, conn) = setup_db()?;
647 let removidos = delete_entities_by_ids(&conn, &[])?;
648 assert_eq!(removidos, 0);
649 Ok(())
650 }
651
652 #[test]
653 fn test_delete_entities_by_ids_removes_valid_entity() -> TestResult {
654 let (_tmp, conn) = setup_db()?;
655 let e = new_entity_helper("para-deletar");
656 let entity_id = upsert_entity(&conn, "global", &e)?;
657
658 let removidos = delete_entities_by_ids(&conn, &[entity_id])?;
659 assert_eq!(removidos, 1);
660
661 let id = find_entity_id(&conn, "global", "para-deletar")?;
662 assert_eq!(id, None, "entidade deve ter sido removida");
663 Ok(())
664 }
665
666 #[test]
667 fn test_delete_entities_by_ids_missing_id_returns_zero() -> TestResult {
668 let (_tmp, conn) = setup_db()?;
669 let removidos = delete_entities_by_ids(&conn, &[9999])?;
670 assert_eq!(removidos, 0);
671 Ok(())
672 }
673
674 #[test]
675 fn test_delete_entities_by_ids_removes_multiple() -> TestResult {
676 let (_tmp, conn) = setup_db()?;
677 let id1 = upsert_entity(&conn, "global", &new_entity_helper("del-a"))?;
678 let id2 = upsert_entity(&conn, "global", &new_entity_helper("del-b"))?;
679 let id3 = upsert_entity(&conn, "global", &new_entity_helper("del-c"))?;
680
681 let removidos = delete_entities_by_ids(&conn, &[id1, id2])?;
682 assert_eq!(removidos, 2);
683
684 assert!(find_entity_id(&conn, "global", "del-a")?.is_none());
685 assert!(find_entity_id(&conn, "global", "del-b")?.is_none());
686 assert!(find_entity_id(&conn, "global", "del-c")?.is_some());
687 let _ = id3;
688 Ok(())
689 }
690
691 #[test]
692 fn test_delete_entities_by_ids_also_removes_vec() -> TestResult {
693 let (_tmp, conn) = setup_db()?;
694 let e = new_entity_helper("del-com-vec");
695 let entity_id = upsert_entity(&conn, "global", &e)?;
696 let emb = embedding_zero();
697 upsert_entity_vec(&conn, entity_id, "global", "project", &emb, "del-com-vec")?;
698
699 let count_antes: i64 = conn.query_row(
700 "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
701 params![entity_id],
702 |r| r.get(0),
703 )?;
704 assert_eq!(count_antes, 1);
705
706 delete_entities_by_ids(&conn, &[entity_id])?;
707
708 let count_depois: i64 = conn.query_row(
709 "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
710 params![entity_id],
711 |r| r.get(0),
712 )?;
713 assert_eq!(
714 count_depois, 0,
715 "vec_entities deve ser limpo junto com entities"
716 );
717 Ok(())
718 }
719
720 #[test]
725 fn test_upsert_relationship_creates_new() -> TestResult {
726 let (_tmp, conn) = setup_db()?;
727 let id_a = upsert_entity(&conn, "global", &new_entity_helper("rel-a"))?;
728 let id_b = upsert_entity(&conn, "global", &new_entity_helper("rel-b"))?;
729
730 let rel = NewRelationship {
731 source: "rel-a".to_string(),
732 target: "rel-b".to_string(),
733 relation: "uses".to_string(),
734 strength: 0.8,
735 description: None,
736 };
737 let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
738 assert!(rel_id > 0);
739 Ok(())
740 }
741
742 #[test]
743 fn test_upsert_relationship_idempotent() -> TestResult {
744 let (_tmp, conn) = setup_db()?;
745 let id_a = upsert_entity(&conn, "global", &new_entity_helper("idem-a"))?;
746 let id_b = upsert_entity(&conn, "global", &new_entity_helper("idem-b"))?;
747
748 let rel = NewRelationship {
749 source: "idem-a".to_string(),
750 target: "idem-b".to_string(),
751 relation: "uses".to_string(),
752 strength: 0.5,
753 description: None,
754 };
755 let id1 = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
756 let id2 = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
757 assert_eq!(id1, id2);
758 Ok(())
759 }
760
761 #[test]
762 fn test_find_relationship_existing() -> TestResult {
763 let (_tmp, conn) = setup_db()?;
764 let id_a = upsert_entity(&conn, "global", &new_entity_helper("fr-a"))?;
765 let id_b = upsert_entity(&conn, "global", &new_entity_helper("fr-b"))?;
766
767 let rel = NewRelationship {
768 source: "fr-a".to_string(),
769 target: "fr-b".to_string(),
770 relation: "depends_on".to_string(),
771 strength: 0.7,
772 description: None,
773 };
774 upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
775
776 let encontrada = find_relationship(&conn, id_a, id_b, "depends_on")?;
777 let row = encontrada.ok_or("relationship should exist")?;
778 assert_eq!(row.source_id, id_a);
779 assert_eq!(row.target_id, id_b);
780 assert!((row.weight - 0.7).abs() < 1e-9);
781 Ok(())
782 }
783
784 #[test]
785 fn test_find_relationship_missing_returns_none() -> TestResult {
786 let (_tmp, conn) = setup_db()?;
787 let resultado = find_relationship(&conn, 9999, 8888, "uses")?;
788 assert!(resultado.is_none());
789 Ok(())
790 }
791
792 #[test]
797 fn test_link_memory_entity_idempotent() -> TestResult {
798 let (_tmp, conn) = setup_db()?;
799 let memory_id = insert_memory(&conn)?;
800 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("me-ent"))?;
801
802 link_memory_entity(&conn, memory_id, entity_id)?;
803 let resultado = link_memory_entity(&conn, memory_id, entity_id);
804 assert!(
805 resultado.is_ok(),
806 "INSERT OR IGNORE must not fail on duplicate"
807 );
808 Ok(())
809 }
810
811 #[test]
812 fn test_link_memory_relationship_idempotent() -> TestResult {
813 let (_tmp, conn) = setup_db()?;
814 let memory_id = insert_memory(&conn)?;
815 let id_a = upsert_entity(&conn, "global", &new_entity_helper("mr-a"))?;
816 let id_b = upsert_entity(&conn, "global", &new_entity_helper("mr-b"))?;
817
818 let rel = NewRelationship {
819 source: "mr-a".to_string(),
820 target: "mr-b".to_string(),
821 relation: "uses".to_string(),
822 strength: 0.5,
823 description: None,
824 };
825 let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
826
827 link_memory_relationship(&conn, memory_id, rel_id)?;
828 let resultado = link_memory_relationship(&conn, memory_id, rel_id);
829 assert!(
830 resultado.is_ok(),
831 "INSERT OR IGNORE must not fail on duplicate"
832 );
833 Ok(())
834 }
835
836 #[test]
841 fn test_increment_degree_increases_counter() -> TestResult {
842 let (_tmp, conn) = setup_db()?;
843 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("grau-ent"))?;
844
845 increment_degree(&conn, entity_id)?;
846 increment_degree(&conn, entity_id)?;
847
848 let degree: i64 = conn.query_row(
849 "SELECT degree FROM entities WHERE id = ?1",
850 params![entity_id],
851 |r| r.get(0),
852 )?;
853 assert_eq!(degree, 2);
854 Ok(())
855 }
856
857 #[test]
858 fn test_recalculate_degree_reflects_actual_relations() -> TestResult {
859 let (_tmp, conn) = setup_db()?;
860 let id_a = upsert_entity(&conn, "global", &new_entity_helper("rc-a"))?;
861 let id_b = upsert_entity(&conn, "global", &new_entity_helper("rc-b"))?;
862 let id_c = upsert_entity(&conn, "global", &new_entity_helper("rc-c"))?;
863
864 let rel1 = NewRelationship {
865 source: "rc-a".to_string(),
866 target: "rc-b".to_string(),
867 relation: "uses".to_string(),
868 strength: 0.5,
869 description: None,
870 };
871 let rel2 = NewRelationship {
872 source: "rc-c".to_string(),
873 target: "rc-a".to_string(),
874 relation: "depends_on".to_string(),
875 strength: 0.5,
876 description: None,
877 };
878 upsert_relationship(&conn, "global", id_a, id_b, &rel1)?;
879 upsert_relationship(&conn, "global", id_c, id_a, &rel2)?;
880
881 recalculate_degree(&conn, id_a)?;
882
883 let degree: i64 = conn.query_row(
884 "SELECT degree FROM entities WHERE id = ?1",
885 params![id_a],
886 |r| r.get(0),
887 )?;
888 assert_eq!(
889 degree, 2,
890 "rc-a appears in two relationships (source+target)"
891 );
892 Ok(())
893 }
894
895 #[test]
900 fn test_find_orphan_entity_ids_without_orphans() -> TestResult {
901 let (_tmp, conn) = setup_db()?;
902 let memory_id = insert_memory(&conn)?;
903 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("nao-orfa"))?;
904 link_memory_entity(&conn, memory_id, entity_id)?;
905
906 let orfas = find_orphan_entity_ids(&conn, Some("global"))?;
907 assert!(!orfas.contains(&entity_id));
908 Ok(())
909 }
910
911 #[test]
912 fn test_find_orphan_entity_ids_detects_orphans() -> TestResult {
913 let (_tmp, conn) = setup_db()?;
914 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("sim-orfa"))?;
915
916 let orfas = find_orphan_entity_ids(&conn, Some("global"))?;
917 assert!(orfas.contains(&entity_id));
918 Ok(())
919 }
920
921 #[test]
922 fn test_find_orphan_entity_ids_without_namespace_returns_all() -> TestResult {
923 let (_tmp, conn) = setup_db()?;
924 let id1 = upsert_entity(&conn, "ns-a", &new_entity_helper("orfa-a"))?;
925 let id2 = upsert_entity(&conn, "ns-b", &new_entity_helper("orfa-b"))?;
926
927 let orfas = find_orphan_entity_ids(&conn, None)?;
928 assert!(orfas.contains(&id1));
929 assert!(orfas.contains(&id2));
930 Ok(())
931 }
932
933 #[test]
938 fn test_list_entities_with_namespace() -> TestResult {
939 let (_tmp, conn) = setup_db()?;
940 upsert_entity(&conn, "le-ns", &new_entity_helper("le-ent-1"))?;
941 upsert_entity(&conn, "le-ns", &new_entity_helper("le-ent-2"))?;
942 upsert_entity(&conn, "outro-ns", &new_entity_helper("le-ent-3"))?;
943
944 let lista = list_entities(&conn, Some("le-ns"))?;
945 assert_eq!(lista.len(), 2);
946 assert!(lista.iter().all(|e| e.namespace == "le-ns"));
947 Ok(())
948 }
949
950 #[test]
951 fn test_list_entities_without_namespace_returns_all() -> TestResult {
952 let (_tmp, conn) = setup_db()?;
953 upsert_entity(&conn, "ns1", &new_entity_helper("all-ent-1"))?;
954 upsert_entity(&conn, "ns2", &new_entity_helper("all-ent-2"))?;
955
956 let lista = list_entities(&conn, None)?;
957 assert!(lista.len() >= 2);
958 Ok(())
959 }
960
961 #[test]
962 fn test_list_relationships_by_namespace_filters_correctly() -> TestResult {
963 let (_tmp, conn) = setup_db()?;
964 let id_a = upsert_entity(&conn, "rel-ns", &new_entity_helper("lr-a"))?;
965 let id_b = upsert_entity(&conn, "rel-ns", &new_entity_helper("lr-b"))?;
966
967 let rel = NewRelationship {
968 source: "lr-a".to_string(),
969 target: "lr-b".to_string(),
970 relation: "uses".to_string(),
971 strength: 0.5,
972 description: None,
973 };
974 upsert_relationship(&conn, "rel-ns", id_a, id_b, &rel)?;
975
976 let lista = list_relationships_by_namespace(&conn, Some("rel-ns"))?;
977 assert!(!lista.is_empty());
978 assert!(lista.iter().all(|r| r.namespace == "rel-ns"));
979 Ok(())
980 }
981
982 #[test]
987 fn test_delete_relationship_by_id_removes_relation() -> TestResult {
988 let (_tmp, conn) = setup_db()?;
989 let id_a = upsert_entity(&conn, "global", &new_entity_helper("dr-a"))?;
990 let id_b = upsert_entity(&conn, "global", &new_entity_helper("dr-b"))?;
991
992 let rel = NewRelationship {
993 source: "dr-a".to_string(),
994 target: "dr-b".to_string(),
995 relation: "uses".to_string(),
996 strength: 0.5,
997 description: None,
998 };
999 let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
1000
1001 delete_relationship_by_id(&conn, rel_id)?;
1002
1003 let encontrada = find_relationship(&conn, id_a, id_b, "uses")?;
1004 assert!(encontrada.is_none(), "relationship must have been removed");
1005 Ok(())
1006 }
1007
1008 #[test]
1009 fn test_create_or_fetch_relationship_creates_new() -> TestResult {
1010 let (_tmp, conn) = setup_db()?;
1011 let id_a = upsert_entity(&conn, "global", &new_entity_helper("cf-a"))?;
1012 let id_b = upsert_entity(&conn, "global", &new_entity_helper("cf-b"))?;
1013
1014 let (rel_id, criada) =
1015 create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1016 assert!(rel_id > 0);
1017 assert!(criada);
1018 Ok(())
1019 }
1020
1021 #[test]
1022 fn test_create_or_fetch_relationship_returns_existing() -> TestResult {
1023 let (_tmp, conn) = setup_db()?;
1024 let id_a = upsert_entity(&conn, "global", &new_entity_helper("cf2-a"))?;
1025 let id_b = upsert_entity(&conn, "global", &new_entity_helper("cf2-b"))?;
1026
1027 create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1028 let (_, criada) =
1029 create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1030 assert!(!criada, "second call must return the existing relationship");
1031 Ok(())
1032 }
1033
1034 #[test]
1039 fn accepts_type_field_as_alias() -> TestResult {
1040 let json = r#"{"name": "X", "type": "concept"}"#;
1041 let ent: NewEntity = serde_json::from_str(json)?;
1042 assert_eq!(ent.entity_type, "concept");
1043 Ok(())
1044 }
1045
1046 #[test]
1047 fn accepts_canonical_entity_type_field() -> TestResult {
1048 let json = r#"{"name": "X", "entity_type": "concept"}"#;
1049 let ent: NewEntity = serde_json::from_str(json)?;
1050 assert_eq!(ent.entity_type, "concept");
1051 Ok(())
1052 }
1053
1054 #[test]
1055 fn both_fields_present_yields_duplicate_error() {
1056 let json = r#"{"name": "X", "entity_type": "A", "type": "B"}"#;
1059 let resultado: Result<NewEntity, _> = serde_json::from_str(json);
1060 assert!(
1061 resultado.is_err(),
1062 "both fields in the same JSON are a duplicate"
1063 );
1064 }
1065}