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