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