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 count_relationships_by_relation(
446 conn: &Connection,
447 namespace: &str,
448 relation: &str,
449) -> Result<usize, AppError> {
450 let count: i64 = conn.query_row(
451 "SELECT COUNT(*) FROM relationships WHERE namespace = ?1 AND relation = ?2",
452 params![namespace, relation],
453 |r| r.get(0),
454 )?;
455 Ok(count as usize)
456}
457
458pub fn delete_relationships_by_relation(
469 conn: &Connection,
470 namespace: &str,
471 relation: &str,
472) -> Result<(usize, Vec<i64>), AppError> {
473 let mut stmt = conn.prepare(
475 "SELECT DISTINCT source_id FROM relationships WHERE namespace = ?1 AND relation = ?2
476 UNION
477 SELECT DISTINCT target_id FROM relationships WHERE namespace = ?1 AND relation = ?2",
478 )?;
479 let entity_ids: Vec<i64> = stmt
480 .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
481 .collect::<Result<Vec<_>, _>>()?;
482
483 let mut id_stmt =
485 conn.prepare("SELECT id FROM relationships WHERE namespace = ?1 AND relation = ?2")?;
486 let rel_ids: Vec<i64> = id_stmt
487 .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
488 .collect::<Result<Vec<_>, _>>()?;
489
490 let mut total_deleted: usize = 0;
492 for chunk in rel_ids.chunks(1000) {
493 for &rel_id in chunk {
494 conn.execute(
495 "DELETE FROM memory_relationships WHERE relationship_id = ?1",
496 params![rel_id],
497 )?;
498 let affected =
499 conn.execute("DELETE FROM relationships WHERE id = ?1", params![rel_id])?;
500 total_deleted += affected;
501 }
502 }
503
504 for &eid in &entity_ids {
506 recalculate_degree(conn, eid)?;
507 }
508
509 Ok((total_deleted, entity_ids))
510}
511
512pub fn knn_search(
513 conn: &Connection,
514 embedding: &[f32],
515 namespace: &str,
516 k: usize,
517) -> Result<Vec<(i64, f32)>, AppError> {
518 let bytes = f32_to_bytes(embedding);
519 let mut stmt = conn.prepare(
520 "SELECT entity_id, distance FROM vec_entities
521 WHERE embedding MATCH ?1 AND namespace = ?2
522 ORDER BY distance LIMIT ?3",
523 )?;
524 let rows = stmt
525 .query_map(params![bytes, namespace, k as i64], |r| {
526 Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
527 })?
528 .collect::<Result<Vec<_>, _>>()?;
529 Ok(rows)
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::constants::EMBEDDING_DIM;
536 use crate::entity_type::EntityType;
537 use crate::storage::connection::register_vec_extension;
538 use rusqlite::Connection;
539 use tempfile::TempDir;
540
541 type TestResult = Result<(), Box<dyn std::error::Error>>;
542
543 fn setup_db() -> Result<(TempDir, Connection), Box<dyn std::error::Error>> {
544 register_vec_extension();
545 let tmp = TempDir::new()?;
546 let db_path = tmp.path().join("test.db");
547 let mut conn = Connection::open(&db_path)?;
548 crate::migrations::runner().run(&mut conn)?;
549 Ok((tmp, conn))
550 }
551
552 fn insert_memory(conn: &Connection) -> Result<i64, Box<dyn std::error::Error>> {
553 conn.execute(
554 "INSERT INTO memories (namespace, name, type, description, body, body_hash)
555 VALUES ('global', 'test-mem', 'user', 'desc', 'body', 'hash1')",
556 [],
557 )?;
558 Ok(conn.last_insert_rowid())
559 }
560
561 fn new_entity_helper(name: &str) -> NewEntity {
562 NewEntity {
563 name: name.to_string(),
564 entity_type: EntityType::Project,
565 description: None,
566 }
567 }
568
569 fn embedding_zero() -> Vec<f32> {
570 vec![0.0f32; EMBEDDING_DIM]
571 }
572
573 #[test]
578 fn test_upsert_entity_creates_new() -> TestResult {
579 let (_tmp, conn) = setup_db()?;
580 let e = new_entity_helper("projeto-alpha");
581 let id = upsert_entity(&conn, "global", &e)?;
582 assert!(id > 0);
583 Ok(())
584 }
585
586 #[test]
587 fn test_upsert_entity_idempotent_returns_same_id() -> TestResult {
588 let (_tmp, conn) = setup_db()?;
589 let e = new_entity_helper("projeto-beta");
590 let id1 = upsert_entity(&conn, "global", &e)?;
591 let id2 = upsert_entity(&conn, "global", &e)?;
592 assert_eq!(id1, id2);
593 Ok(())
594 }
595
596 #[test]
597 fn test_upsert_entity_updates_description() -> TestResult {
598 let (_tmp, conn) = setup_db()?;
599 let e1 = new_entity_helper("projeto-gamma");
600 let id1 = upsert_entity(&conn, "global", &e1)?;
601
602 let e2 = NewEntity {
603 name: "projeto-gamma".to_string(),
604 entity_type: EntityType::Tool,
605 description: Some("nova desc".to_string()),
606 };
607 let id2 = upsert_entity(&conn, "global", &e2)?;
608 assert_eq!(id1, id2);
609
610 let desc: Option<String> = conn.query_row(
611 "SELECT description FROM entities WHERE id = ?1",
612 params![id1],
613 |r| r.get(0),
614 )?;
615 assert_eq!(desc.as_deref(), Some("nova desc"));
616 Ok(())
617 }
618
619 #[test]
620 fn test_upsert_entity_different_namespaces_create_distinct_records() -> TestResult {
621 let (_tmp, conn) = setup_db()?;
622 let e = new_entity_helper("compartilhada");
623 let id1 = upsert_entity(&conn, "ns1", &e)?;
624 let id2 = upsert_entity(&conn, "ns2", &e)?;
625 assert_ne!(id1, id2);
626 Ok(())
627 }
628
629 #[test]
634 fn test_upsert_entity_vec_first_time_without_conflict() -> TestResult {
635 let (_tmp, conn) = setup_db()?;
636 let e = new_entity_helper("vec-nova");
637 let entity_id = upsert_entity(&conn, "global", &e)?;
638 let emb = embedding_zero();
639
640 let result = upsert_entity_vec(
641 &conn,
642 entity_id,
643 "global",
644 EntityType::Project,
645 &emb,
646 "vec-nova",
647 );
648 assert!(result.is_ok(), "first insertion must succeed");
649
650 let count: i64 = conn.query_row(
651 "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
652 params![entity_id],
653 |r| r.get(0),
654 )?;
655 assert_eq!(count, 1, "must have exactly one row after insertion");
656 Ok(())
657 }
658
659 #[test]
660 fn test_upsert_entity_vec_second_time_replaces_without_error() -> TestResult {
661 let (_tmp, conn) = setup_db()?;
663 let e = new_entity_helper("vec-existente");
664 let entity_id = upsert_entity(&conn, "global", &e)?;
665 let emb = embedding_zero();
666
667 upsert_entity_vec(
668 &conn,
669 entity_id,
670 "global",
671 EntityType::Project,
672 &emb,
673 "vec-existente",
674 )?;
675
676 let result = upsert_entity_vec(
678 &conn,
679 entity_id,
680 "global",
681 EntityType::Tool,
682 &emb,
683 "vec-existente",
684 );
685 assert!(
686 result.is_ok(),
687 "second insertion (replace) must succeed: {result:?}"
688 );
689
690 let count: i64 = conn.query_row(
691 "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
692 params![entity_id],
693 |r| r.get(0),
694 )?;
695 assert_eq!(count, 1, "must have exactly one row after replacement");
696 Ok(())
697 }
698
699 #[test]
700 fn test_upsert_entity_vec_multiple_independent_entities() -> TestResult {
701 let (_tmp, conn) = setup_db()?;
702 let emb = embedding_zero();
703
704 for i in 0..3i64 {
705 let nome = format!("ent-{i}");
706 let e = new_entity_helper(&nome);
707 let entity_id = upsert_entity(&conn, "global", &e)?;
708 upsert_entity_vec(&conn, entity_id, "global", EntityType::Project, &emb, &nome)?;
709 }
710
711 let count: i64 = conn.query_row("SELECT COUNT(*) FROM vec_entities", [], |r| r.get(0))?;
712 assert_eq!(count, 3, "must have three distinct rows in vec_entities");
713 Ok(())
714 }
715
716 #[test]
721 fn test_find_entity_id_existing_returns_some() -> TestResult {
722 let (_tmp, conn) = setup_db()?;
723 let e = new_entity_helper("entidade-busca");
724 let id_inserido = upsert_entity(&conn, "global", &e)?;
725 let id_encontrado = find_entity_id(&conn, "global", "entidade-busca")?;
726 assert_eq!(id_encontrado, Some(id_inserido));
727 Ok(())
728 }
729
730 #[test]
731 fn test_find_entity_id_missing_returns_none() -> TestResult {
732 let (_tmp, conn) = setup_db()?;
733 let id = find_entity_id(&conn, "global", "nao-existe")?;
734 assert_eq!(id, None);
735 Ok(())
736 }
737
738 #[test]
743 fn test_delete_entities_by_ids_empty_list_returns_zero() -> TestResult {
744 let (_tmp, conn) = setup_db()?;
745 let removed = delete_entities_by_ids(&conn, &[])?;
746 assert_eq!(removed, 0);
747 Ok(())
748 }
749
750 #[test]
751 fn test_delete_entities_by_ids_removes_valid_entity() -> TestResult {
752 let (_tmp, conn) = setup_db()?;
753 let e = new_entity_helper("to-delete");
754 let entity_id = upsert_entity(&conn, "global", &e)?;
755
756 let removed = delete_entities_by_ids(&conn, &[entity_id])?;
757 assert_eq!(removed, 1);
758
759 let id = find_entity_id(&conn, "global", "to-delete")?;
760 assert_eq!(id, None, "entity must have been removed");
761 Ok(())
762 }
763
764 #[test]
765 fn test_delete_entities_by_ids_missing_id_returns_zero() -> TestResult {
766 let (_tmp, conn) = setup_db()?;
767 let removed = delete_entities_by_ids(&conn, &[9999])?;
768 assert_eq!(removed, 0);
769 Ok(())
770 }
771
772 #[test]
773 fn test_delete_entities_by_ids_removes_multiple() -> TestResult {
774 let (_tmp, conn) = setup_db()?;
775 let id1 = upsert_entity(&conn, "global", &new_entity_helper("del-a"))?;
776 let id2 = upsert_entity(&conn, "global", &new_entity_helper("del-b"))?;
777 let id3 = upsert_entity(&conn, "global", &new_entity_helper("del-c"))?;
778
779 let removed = delete_entities_by_ids(&conn, &[id1, id2])?;
780 assert_eq!(removed, 2);
781
782 assert!(find_entity_id(&conn, "global", "del-a")?.is_none());
783 assert!(find_entity_id(&conn, "global", "del-b")?.is_none());
784 assert!(find_entity_id(&conn, "global", "del-c")?.is_some());
785 let _ = id3;
786 Ok(())
787 }
788
789 #[test]
790 fn test_delete_entities_by_ids_also_removes_vec() -> TestResult {
791 let (_tmp, conn) = setup_db()?;
792 let e = new_entity_helper("del-com-vec");
793 let entity_id = upsert_entity(&conn, "global", &e)?;
794 let emb = embedding_zero();
795 upsert_entity_vec(
796 &conn,
797 entity_id,
798 "global",
799 EntityType::Project,
800 &emb,
801 "del-com-vec",
802 )?;
803
804 let count_antes: i64 = conn.query_row(
805 "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
806 params![entity_id],
807 |r| r.get(0),
808 )?;
809 assert_eq!(count_antes, 1);
810
811 delete_entities_by_ids(&conn, &[entity_id])?;
812
813 let count_depois: i64 = conn.query_row(
814 "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
815 params![entity_id],
816 |r| r.get(0),
817 )?;
818 assert_eq!(
819 count_depois, 0,
820 "vec_entities deve ser limpo junto com entities"
821 );
822 Ok(())
823 }
824
825 #[test]
830 fn test_upsert_relationship_creates_new() -> TestResult {
831 let (_tmp, conn) = setup_db()?;
832 let id_a = upsert_entity(&conn, "global", &new_entity_helper("rel-a"))?;
833 let id_b = upsert_entity(&conn, "global", &new_entity_helper("rel-b"))?;
834
835 let rel = NewRelationship {
836 source: "rel-a".to_string(),
837 target: "rel-b".to_string(),
838 relation: "uses".to_string(),
839 strength: 0.8,
840 description: None,
841 };
842 let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
843 assert!(rel_id > 0);
844 Ok(())
845 }
846
847 #[test]
848 fn test_upsert_relationship_idempotent() -> TestResult {
849 let (_tmp, conn) = setup_db()?;
850 let id_a = upsert_entity(&conn, "global", &new_entity_helper("idem-a"))?;
851 let id_b = upsert_entity(&conn, "global", &new_entity_helper("idem-b"))?;
852
853 let rel = NewRelationship {
854 source: "idem-a".to_string(),
855 target: "idem-b".to_string(),
856 relation: "uses".to_string(),
857 strength: 0.5,
858 description: None,
859 };
860 let id1 = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
861 let id2 = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
862 assert_eq!(id1, id2);
863 Ok(())
864 }
865
866 #[test]
867 fn test_find_relationship_existing() -> TestResult {
868 let (_tmp, conn) = setup_db()?;
869 let id_a = upsert_entity(&conn, "global", &new_entity_helper("fr-a"))?;
870 let id_b = upsert_entity(&conn, "global", &new_entity_helper("fr-b"))?;
871
872 let rel = NewRelationship {
873 source: "fr-a".to_string(),
874 target: "fr-b".to_string(),
875 relation: "depends_on".to_string(),
876 strength: 0.7,
877 description: None,
878 };
879 upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
880
881 let encontrada = find_relationship(&conn, id_a, id_b, "depends_on")?;
882 let row = encontrada.ok_or("relationship should exist")?;
883 assert_eq!(row.source_id, id_a);
884 assert_eq!(row.target_id, id_b);
885 assert!((row.weight - 0.7).abs() < 1e-9);
886 Ok(())
887 }
888
889 #[test]
890 fn test_find_relationship_missing_returns_none() -> TestResult {
891 let (_tmp, conn) = setup_db()?;
892 let resultado = find_relationship(&conn, 9999, 8888, "uses")?;
893 assert!(resultado.is_none());
894 Ok(())
895 }
896
897 #[test]
902 fn test_link_memory_entity_idempotent() -> TestResult {
903 let (_tmp, conn) = setup_db()?;
904 let memory_id = insert_memory(&conn)?;
905 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("me-ent"))?;
906
907 link_memory_entity(&conn, memory_id, entity_id)?;
908 let resultado = link_memory_entity(&conn, memory_id, entity_id);
909 assert!(
910 resultado.is_ok(),
911 "INSERT OR IGNORE must not fail on duplicate"
912 );
913 Ok(())
914 }
915
916 #[test]
917 fn test_link_memory_relationship_idempotent() -> TestResult {
918 let (_tmp, conn) = setup_db()?;
919 let memory_id = insert_memory(&conn)?;
920 let id_a = upsert_entity(&conn, "global", &new_entity_helper("mr-a"))?;
921 let id_b = upsert_entity(&conn, "global", &new_entity_helper("mr-b"))?;
922
923 let rel = NewRelationship {
924 source: "mr-a".to_string(),
925 target: "mr-b".to_string(),
926 relation: "uses".to_string(),
927 strength: 0.5,
928 description: None,
929 };
930 let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
931
932 link_memory_relationship(&conn, memory_id, rel_id)?;
933 let resultado = link_memory_relationship(&conn, memory_id, rel_id);
934 assert!(
935 resultado.is_ok(),
936 "INSERT OR IGNORE must not fail on duplicate"
937 );
938 Ok(())
939 }
940
941 #[test]
946 fn test_increment_degree_increases_counter() -> TestResult {
947 let (_tmp, conn) = setup_db()?;
948 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("grau-ent"))?;
949
950 increment_degree(&conn, entity_id)?;
951 increment_degree(&conn, entity_id)?;
952
953 let degree: i64 = conn.query_row(
954 "SELECT degree FROM entities WHERE id = ?1",
955 params![entity_id],
956 |r| r.get(0),
957 )?;
958 assert_eq!(degree, 2);
959 Ok(())
960 }
961
962 #[test]
963 fn test_recalculate_degree_reflects_actual_relations() -> TestResult {
964 let (_tmp, conn) = setup_db()?;
965 let id_a = upsert_entity(&conn, "global", &new_entity_helper("rc-a"))?;
966 let id_b = upsert_entity(&conn, "global", &new_entity_helper("rc-b"))?;
967 let id_c = upsert_entity(&conn, "global", &new_entity_helper("rc-c"))?;
968
969 let rel1 = NewRelationship {
970 source: "rc-a".to_string(),
971 target: "rc-b".to_string(),
972 relation: "uses".to_string(),
973 strength: 0.5,
974 description: None,
975 };
976 let rel2 = NewRelationship {
977 source: "rc-c".to_string(),
978 target: "rc-a".to_string(),
979 relation: "depends_on".to_string(),
980 strength: 0.5,
981 description: None,
982 };
983 upsert_relationship(&conn, "global", id_a, id_b, &rel1)?;
984 upsert_relationship(&conn, "global", id_c, id_a, &rel2)?;
985
986 recalculate_degree(&conn, id_a)?;
987
988 let degree: i64 = conn.query_row(
989 "SELECT degree FROM entities WHERE id = ?1",
990 params![id_a],
991 |r| r.get(0),
992 )?;
993 assert_eq!(
994 degree, 2,
995 "rc-a appears in two relationships (source+target)"
996 );
997 Ok(())
998 }
999
1000 #[test]
1005 fn test_find_orphan_entity_ids_without_orphans() -> TestResult {
1006 let (_tmp, conn) = setup_db()?;
1007 let memory_id = insert_memory(&conn)?;
1008 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("nao-orfa"))?;
1009 link_memory_entity(&conn, memory_id, entity_id)?;
1010
1011 let orfas = find_orphan_entity_ids(&conn, Some("global"))?;
1012 assert!(!orfas.contains(&entity_id));
1013 Ok(())
1014 }
1015
1016 #[test]
1017 fn test_find_orphan_entity_ids_detects_orphans() -> TestResult {
1018 let (_tmp, conn) = setup_db()?;
1019 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("sim-orfa"))?;
1020
1021 let orfas = find_orphan_entity_ids(&conn, Some("global"))?;
1022 assert!(orfas.contains(&entity_id));
1023 Ok(())
1024 }
1025
1026 #[test]
1027 fn test_find_orphan_entity_ids_without_namespace_returns_all() -> TestResult {
1028 let (_tmp, conn) = setup_db()?;
1029 let id1 = upsert_entity(&conn, "ns-a", &new_entity_helper("orfa-a"))?;
1030 let id2 = upsert_entity(&conn, "ns-b", &new_entity_helper("orfa-b"))?;
1031
1032 let orfas = find_orphan_entity_ids(&conn, None)?;
1033 assert!(orfas.contains(&id1));
1034 assert!(orfas.contains(&id2));
1035 Ok(())
1036 }
1037
1038 #[test]
1043 fn test_list_entities_with_namespace() -> TestResult {
1044 let (_tmp, conn) = setup_db()?;
1045 upsert_entity(&conn, "le-ns", &new_entity_helper("le-ent-1"))?;
1046 upsert_entity(&conn, "le-ns", &new_entity_helper("le-ent-2"))?;
1047 upsert_entity(&conn, "outro-ns", &new_entity_helper("le-ent-3"))?;
1048
1049 let lista = list_entities(&conn, Some("le-ns"))?;
1050 assert_eq!(lista.len(), 2);
1051 assert!(lista.iter().all(|e| e.namespace == "le-ns"));
1052 Ok(())
1053 }
1054
1055 #[test]
1056 fn test_list_entities_without_namespace_returns_all() -> TestResult {
1057 let (_tmp, conn) = setup_db()?;
1058 upsert_entity(&conn, "ns1", &new_entity_helper("all-ent-1"))?;
1059 upsert_entity(&conn, "ns2", &new_entity_helper("all-ent-2"))?;
1060
1061 let lista = list_entities(&conn, None)?;
1062 assert!(lista.len() >= 2);
1063 Ok(())
1064 }
1065
1066 #[test]
1067 fn test_list_relationships_by_namespace_filters_correctly() -> TestResult {
1068 let (_tmp, conn) = setup_db()?;
1069 let id_a = upsert_entity(&conn, "rel-ns", &new_entity_helper("lr-a"))?;
1070 let id_b = upsert_entity(&conn, "rel-ns", &new_entity_helper("lr-b"))?;
1071
1072 let rel = NewRelationship {
1073 source: "lr-a".to_string(),
1074 target: "lr-b".to_string(),
1075 relation: "uses".to_string(),
1076 strength: 0.5,
1077 description: None,
1078 };
1079 upsert_relationship(&conn, "rel-ns", id_a, id_b, &rel)?;
1080
1081 let lista = list_relationships_by_namespace(&conn, Some("rel-ns"))?;
1082 assert!(!lista.is_empty());
1083 assert!(lista.iter().all(|r| r.namespace == "rel-ns"));
1084 Ok(())
1085 }
1086
1087 #[test]
1092 fn test_delete_relationship_by_id_removes_relation() -> TestResult {
1093 let (_tmp, conn) = setup_db()?;
1094 let id_a = upsert_entity(&conn, "global", &new_entity_helper("dr-a"))?;
1095 let id_b = upsert_entity(&conn, "global", &new_entity_helper("dr-b"))?;
1096
1097 let rel = NewRelationship {
1098 source: "dr-a".to_string(),
1099 target: "dr-b".to_string(),
1100 relation: "uses".to_string(),
1101 strength: 0.5,
1102 description: None,
1103 };
1104 let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
1105
1106 delete_relationship_by_id(&conn, rel_id)?;
1107
1108 let encontrada = find_relationship(&conn, id_a, id_b, "uses")?;
1109 assert!(encontrada.is_none(), "relationship must have been removed");
1110 Ok(())
1111 }
1112
1113 #[test]
1114 fn test_create_or_fetch_relationship_creates_new() -> TestResult {
1115 let (_tmp, conn) = setup_db()?;
1116 let id_a = upsert_entity(&conn, "global", &new_entity_helper("cf-a"))?;
1117 let id_b = upsert_entity(&conn, "global", &new_entity_helper("cf-b"))?;
1118
1119 let (rel_id, created) =
1120 create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1121 assert!(rel_id > 0);
1122 assert!(created);
1123 Ok(())
1124 }
1125
1126 #[test]
1127 fn test_create_or_fetch_relationship_returns_existing() -> TestResult {
1128 let (_tmp, conn) = setup_db()?;
1129 let id_a = upsert_entity(&conn, "global", &new_entity_helper("cf2-a"))?;
1130 let id_b = upsert_entity(&conn, "global", &new_entity_helper("cf2-b"))?;
1131
1132 create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1133 let (_, created) =
1134 create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1135 assert!(
1136 !created,
1137 "second call must return the existing relationship"
1138 );
1139 Ok(())
1140 }
1141
1142 #[test]
1147 fn accepts_type_field_as_alias() -> TestResult {
1148 let json = r#"{"name": "X", "type": "concept"}"#;
1149 let ent: NewEntity = serde_json::from_str(json)?;
1150 assert_eq!(ent.entity_type, EntityType::Concept);
1151 Ok(())
1152 }
1153
1154 #[test]
1155 fn accepts_canonical_entity_type_field() -> TestResult {
1156 let json = r#"{"name": "X", "entity_type": "concept"}"#;
1157 let ent: NewEntity = serde_json::from_str(json)?;
1158 assert_eq!(ent.entity_type, EntityType::Concept);
1159 Ok(())
1160 }
1161
1162 #[test]
1163 fn both_fields_present_yields_duplicate_error() {
1164 let json = r#"{"name": "X", "entity_type": "concept", "type": "person"}"#;
1166 let resultado: Result<NewEntity, _> = serde_json::from_str(json);
1167 assert!(
1168 resultado.is_err(),
1169 "both fields in the same JSON are a duplicate"
1170 );
1171 }
1172}