1use crate::embedder::f32_to_bytes;
8use crate::entity_type::EntityType;
9use crate::errors::AppError;
10use crate::parsers::normalize_entity_name;
11use crate::storage::utils::with_busy_retry;
12use rusqlite::{params, Connection};
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Serialize, Deserialize, Clone)]
20#[serde(deny_unknown_fields)]
21pub struct NewEntity {
22 pub name: String,
23 #[serde(alias = "type")]
24 pub entity_type: EntityType,
25 pub description: Option<String>,
26}
27
28#[derive(Debug, Serialize, Deserialize, Clone)]
33#[serde(deny_unknown_fields)]
34pub struct NewRelationship {
35 #[serde(alias = "from")]
36 pub source: String,
37 #[serde(alias = "to")]
38 pub target: String,
39 #[serde(alias = "type")]
40 pub relation: String,
41 #[serde(alias = "weight")]
42 pub strength: f64,
43 pub description: Option<String>,
44}
45
46pub fn validate_entity_name(name: &str) -> Result<(), AppError> {
55 if name.len() < 2 {
56 return Err(AppError::Validation(format!(
57 "entity name '{name}' must be at least 2 characters"
58 )));
59 }
60 if name.contains('\n') || name.contains('\r') {
61 return Err(AppError::Validation(
62 "entity name must not contain newline characters".to_string(),
63 ));
64 }
65 if name.len() <= 4
66 && name
67 .chars()
68 .all(|c| c.is_ascii_uppercase() || c == '_' || c == '-')
69 {
70 return Err(AppError::Validation(format!(
71 "entity name '{name}' rejected: short ALL_CAPS names are typically NER noise"
72 )));
73 }
74 Ok(())
75}
76
77pub fn upsert_entity(conn: &Connection, namespace: &str, e: &NewEntity) -> Result<i64, AppError> {
86 validate_entity_name(&e.name)?;
89 let normalized_name = normalize_entity_name(&e.name);
91 if normalized_name.chars().count() < 2 {
94 return Err(AppError::Validation(format!(
95 "entity name '{}' normalizes to '{}' which is too short (minimum 2 characters)",
96 e.name, normalized_name
97 )));
98 }
99 conn.execute(
100 "INSERT INTO entities (namespace, name, type, description)
101 VALUES (?1, ?2, ?3, ?4)
102 ON CONFLICT(namespace, name) DO UPDATE SET
103 type = excluded.type,
104 description = COALESCE(excluded.description, entities.description),
105 updated_at = unixepoch()",
106 params![namespace, normalized_name, e.entity_type, e.description],
107 )?;
108 let id: i64 = conn.query_row(
109 "SELECT id FROM entities WHERE namespace = ?1 AND name = ?2",
110 params![namespace, normalized_name],
111 |r| r.get(0),
112 )?;
113 Ok(id)
114}
115
116pub fn upsert_entity_vec(
127 conn: &Connection,
128 entity_id: i64,
129 namespace: &str,
130 _entity_type: EntityType,
131 embedding: &[f32],
132 _name: &str,
133) -> Result<(), AppError> {
134 let embedding_bytes = f32_to_bytes(embedding);
135 with_busy_retry(|| {
136 conn.execute(
137 "DELETE FROM entity_embeddings WHERE entity_id = ?1",
138 params![entity_id],
139 )?;
140 conn.execute(
141 "INSERT INTO entity_embeddings(entity_id, namespace, embedding, source, model, dim)
142 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
143 params![
144 entity_id,
145 namespace,
146 &embedding_bytes,
147 "llm-headless",
148 crate::constants::SQLITE_GRAPHRAG_VERSION,
149 crate::constants::embedding_dim() as i64,
150 ],
151 )?;
152 Ok(())
153 })
154}
155
156pub fn upsert_relationship(
165 conn: &Connection,
166 namespace: &str,
167 source_id: i64,
168 target_id: i64,
169 rel: &NewRelationship,
170) -> Result<i64, AppError> {
171 conn.execute(
172 "INSERT INTO relationships (namespace, source_id, target_id, relation, weight, description)
173 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
174 ON CONFLICT(source_id, target_id, relation) DO UPDATE SET
175 weight = excluded.weight,
176 description = COALESCE(excluded.description, relationships.description)",
177 params![
178 namespace,
179 source_id,
180 target_id,
181 rel.relation,
182 rel.strength,
183 rel.description
184 ],
185 )?;
186 let id: i64 = conn.query_row(
187 "SELECT id FROM relationships WHERE source_id=?1 AND target_id=?2 AND relation=?3",
188 params![source_id, target_id, rel.relation],
189 |r| r.get(0),
190 )?;
191 Ok(id)
192}
193
194pub fn link_memory_entity(
200 conn: &Connection,
201 memory_id: i64,
202 entity_id: i64,
203) -> Result<(), AppError> {
204 conn.execute(
205 "INSERT OR IGNORE INTO memory_entities (memory_id, entity_id) VALUES (?1, ?2)",
206 params![memory_id, entity_id],
207 )?;
208 Ok(())
209}
210
211pub fn link_memory_relationship(
217 conn: &Connection,
218 memory_id: i64,
219 rel_id: i64,
220) -> Result<(), AppError> {
221 conn.execute(
222 "INSERT OR IGNORE INTO memory_relationships (memory_id, relationship_id) VALUES (?1, ?2)",
223 params![memory_id, rel_id],
224 )?;
225 Ok(())
226}
227
228pub fn increment_degree(conn: &Connection, entity_id: i64) -> Result<(), AppError> {
234 conn.execute(
235 "UPDATE entities SET degree = degree + 1 WHERE id = ?1",
236 params![entity_id],
237 )?;
238 Ok(())
239}
240
241pub fn find_entity_id(
247 conn: &Connection,
248 namespace: &str,
249 name: &str,
250) -> Result<Option<i64>, AppError> {
251 let name = normalize_entity_name(name);
257 let mut stmt =
258 conn.prepare_cached("SELECT id FROM entities WHERE namespace = ?1 AND name = ?2")?;
259 match stmt.query_row(params![namespace, &name], |r| r.get::<_, i64>(0)) {
260 Ok(id) => Ok(Some(id)),
261 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
262 Err(e) => Err(AppError::Database(e)),
263 }
264}
265
266#[derive(Debug, Serialize)]
268pub struct RelationshipRow {
269 pub id: i64,
270 pub namespace: String,
271 pub source_id: i64,
272 pub target_id: i64,
273 pub relation: String,
274 pub weight: f64,
275 pub description: Option<String>,
276}
277
278pub fn find_relationship(
284 conn: &Connection,
285 source_id: i64,
286 target_id: i64,
287 relation: &str,
288) -> Result<Option<RelationshipRow>, AppError> {
289 let mut stmt = conn.prepare_cached(
290 "SELECT id, namespace, source_id, target_id, relation, weight, description
291 FROM relationships
292 WHERE source_id = ?1 AND target_id = ?2 AND relation = ?3",
293 )?;
294 match stmt.query_row(params![source_id, target_id, relation], |r| {
295 Ok(RelationshipRow {
296 id: r.get(0)?,
297 namespace: r.get(1)?,
298 source_id: r.get(2)?,
299 target_id: r.get(3)?,
300 relation: r.get(4)?,
301 weight: r.get(5)?,
302 description: r.get(6)?,
303 })
304 }) {
305 Ok(row) => Ok(Some(row)),
306 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
307 Err(e) => Err(AppError::Database(e)),
308 }
309}
310
311pub fn create_or_fetch_relationship(
319 conn: &Connection,
320 namespace: &str,
321 source_id: i64,
322 target_id: i64,
323 relation: &str,
324 weight: f64,
325 description: Option<&str>,
326) -> Result<(i64, bool), AppError> {
327 let existing = find_relationship(conn, source_id, target_id, relation)?;
329 if let Some(row) = existing {
330 if (row.weight - weight).abs() > f64::EPSILON {
331 conn.execute(
332 "UPDATE relationships SET weight = ?1 WHERE id = ?2",
333 params![weight, row.id],
334 )?;
335 }
336 return Ok((row.id, false));
337 }
338 conn.execute(
339 "INSERT INTO relationships (namespace, source_id, target_id, relation, weight, description)
340 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
341 params![
342 namespace,
343 source_id,
344 target_id,
345 relation,
346 weight,
347 description
348 ],
349 )?;
350 let id: i64 = conn.query_row(
351 "SELECT id FROM relationships WHERE source_id = ?1 AND target_id = ?2 AND relation = ?3",
352 params![source_id, target_id, relation],
353 |r| r.get(0),
354 )?;
355 Ok((id, true))
356}
357
358pub fn delete_relationship_by_id(conn: &Connection, relationship_id: i64) -> Result<(), AppError> {
364 conn.execute(
365 "DELETE FROM memory_relationships WHERE relationship_id = ?1",
366 params![relationship_id],
367 )?;
368 conn.execute(
369 "DELETE FROM relationships WHERE id = ?1",
370 params![relationship_id],
371 )?;
372 Ok(())
373}
374
375pub fn recalculate_degree(conn: &Connection, entity_id: i64) -> Result<(), AppError> {
381 conn.execute(
382 "UPDATE entities
383 SET degree = (SELECT COUNT(*) FROM relationships
384 WHERE source_id = entities.id OR target_id = entities.id)
385 WHERE id = ?1",
386 params![entity_id],
387 )?;
388 Ok(())
389}
390
391#[derive(Debug, Serialize, Clone)]
393pub struct EntityNode {
394 pub id: i64,
395 pub name: String,
396 pub namespace: String,
397 pub kind: String,
398}
399
400pub fn list_entities(
406 conn: &Connection,
407 namespace: Option<&str>,
408) -> Result<Vec<EntityNode>, AppError> {
409 if let Some(ns) = namespace {
410 let mut stmt = conn.prepare_cached(
411 "SELECT id, name, namespace, type FROM entities WHERE namespace = ?1 ORDER BY id",
412 )?;
413 let rows = stmt
414 .query_map(params![ns], |r| {
415 Ok(EntityNode {
416 id: r.get(0)?,
417 name: r.get(1)?,
418 namespace: r.get(2)?,
419 kind: r.get(3)?,
420 })
421 })?
422 .collect::<Result<Vec<_>, _>>()?;
423 Ok(rows)
424 } else {
425 let mut stmt = conn.prepare_cached(
426 "SELECT id, name, namespace, type FROM entities ORDER BY namespace, id",
427 )?;
428 let rows = stmt
429 .query_map([], |r| {
430 Ok(EntityNode {
431 id: r.get(0)?,
432 name: r.get(1)?,
433 namespace: r.get(2)?,
434 kind: r.get(3)?,
435 })
436 })?
437 .collect::<Result<Vec<_>, _>>()?;
438 Ok(rows)
439 }
440}
441
442pub fn list_relationships_by_namespace(
448 conn: &Connection,
449 namespace: Option<&str>,
450) -> Result<Vec<RelationshipRow>, AppError> {
451 if let Some(ns) = namespace {
452 let mut stmt = conn.prepare_cached(
453 "SELECT r.id, r.namespace, r.source_id, r.target_id, r.relation, r.weight, r.description
454 FROM relationships r
455 JOIN entities se ON se.id = r.source_id AND se.namespace = ?1
456 JOIN entities te ON te.id = r.target_id AND te.namespace = ?1
457 ORDER BY r.id",
458 )?;
459 let rows = stmt
460 .query_map(params![ns], |r| {
461 Ok(RelationshipRow {
462 id: r.get(0)?,
463 namespace: r.get(1)?,
464 source_id: r.get(2)?,
465 target_id: r.get(3)?,
466 relation: r.get(4)?,
467 weight: r.get(5)?,
468 description: r.get(6)?,
469 })
470 })?
471 .collect::<Result<Vec<_>, _>>()?;
472 Ok(rows)
473 } else {
474 let mut stmt = conn.prepare_cached(
475 "SELECT id, namespace, source_id, target_id, relation, weight, description
476 FROM relationships ORDER BY id",
477 )?;
478 let rows = stmt
479 .query_map([], |r| {
480 Ok(RelationshipRow {
481 id: r.get(0)?,
482 namespace: r.get(1)?,
483 source_id: r.get(2)?,
484 target_id: r.get(3)?,
485 relation: r.get(4)?,
486 weight: r.get(5)?,
487 description: r.get(6)?,
488 })
489 })?
490 .collect::<Result<Vec<_>, _>>()?;
491 Ok(rows)
492 }
493}
494
495pub fn find_orphan_entity_ids(
501 conn: &Connection,
502 namespace: Option<&str>,
503) -> Result<Vec<i64>, AppError> {
504 if let Some(ns) = namespace {
505 let mut stmt = conn.prepare_cached(
506 "SELECT e.id FROM entities e
507 WHERE e.namespace = ?1
508 AND NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
509 AND NOT EXISTS (
510 SELECT 1 FROM relationships r
511 WHERE r.source_id = e.id OR r.target_id = e.id
512 )",
513 )?;
514 let ids = stmt
515 .query_map(params![ns], |r| r.get::<_, i64>(0))?
516 .collect::<Result<Vec<_>, _>>()?;
517 Ok(ids)
518 } else {
519 let mut stmt = conn.prepare_cached(
520 "SELECT e.id FROM entities e
521 WHERE NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
522 AND NOT EXISTS (
523 SELECT 1 FROM relationships r
524 WHERE r.source_id = e.id OR r.target_id = e.id
525 )",
526 )?;
527 let ids = stmt
528 .query_map([], |r| r.get::<_, i64>(0))?
529 .collect::<Result<Vec<_>, _>>()?;
530 Ok(ids)
531 }
532}
533
534pub fn delete_entities_by_ids(conn: &Connection, entity_ids: &[i64]) -> Result<usize, AppError> {
540 if entity_ids.is_empty() {
541 return Ok(0);
542 }
543 let mut removed = 0usize;
544 for id in entity_ids {
545 let _ = conn.execute("DELETE FROM vec_entities WHERE entity_id = ?1", params![id]);
547 let affected = conn.execute("DELETE FROM entities WHERE id = ?1", params![id])?;
548 removed += affected;
549 }
550 Ok(removed)
551}
552
553pub fn count_relationships_by_relation(
562 conn: &Connection,
563 namespace: &str,
564 relation: &str,
565) -> Result<usize, AppError> {
566 let count: i64 = conn.query_row(
567 "SELECT COUNT(*) FROM relationships WHERE namespace = ?1 AND relation = ?2",
568 params![namespace, relation],
569 |r| r.get(0),
570 )?;
571 Ok(count as usize)
572}
573
574pub fn list_entity_names_by_relation(
583 conn: &Connection,
584 namespace: &str,
585 relation: &str,
586) -> Result<Vec<String>, AppError> {
587 let mut stmt = conn.prepare_cached(
588 "SELECT DISTINCT e.name FROM entities e
589 INNER JOIN relationships r ON (e.id = r.source_id OR e.id = r.target_id)
590 WHERE r.namespace = ?1 AND r.relation = ?2
591 ORDER BY e.name",
592 )?;
593 let names: Vec<String> = stmt
594 .query_map(params![namespace, relation], |row| row.get(0))?
595 .collect::<Result<Vec<_>, _>>()?;
596 Ok(names)
597}
598
599pub fn delete_relationships_by_relation(
610 conn: &Connection,
611 namespace: &str,
612 relation: &str,
613) -> Result<(usize, Vec<i64>), AppError> {
614 let mut stmt = conn.prepare_cached(
616 "SELECT DISTINCT source_id FROM relationships WHERE namespace = ?1 AND relation = ?2
617 UNION
618 SELECT DISTINCT target_id FROM relationships WHERE namespace = ?1 AND relation = ?2",
619 )?;
620 let entity_ids: Vec<i64> = stmt
621 .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
622 .collect::<Result<Vec<_>, _>>()?;
623
624 let mut id_stmt =
626 conn.prepare_cached("SELECT id FROM relationships WHERE namespace = ?1 AND relation = ?2")?;
627 let rel_ids: Vec<i64> = id_stmt
628 .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
629 .collect::<Result<Vec<_>, _>>()?;
630
631 let mut total_deleted: usize = 0;
633 for chunk in rel_ids.chunks(1000) {
634 for &rel_id in chunk {
635 conn.execute(
636 "DELETE FROM memory_relationships WHERE relationship_id = ?1",
637 params![rel_id],
638 )?;
639 let affected =
640 conn.execute("DELETE FROM relationships WHERE id = ?1", params![rel_id])?;
641 total_deleted += affected;
642 }
643 }
644
645 for &eid in &entity_ids {
647 recalculate_degree(conn, eid)?;
648 }
649
650 Ok((total_deleted, entity_ids))
651}
652
653pub fn knn_search(
666 conn: &Connection,
667 embedding: &[f32],
668 namespace: &str,
669 k: usize,
670) -> Result<Vec<(i64, f32)>, AppError> {
671 if embedding.len() != crate::constants::embedding_dim() {
672 return Err(AppError::Embedding(format!(
673 "knn_search embedding has {} dims, expected {}",
674 embedding.len(),
675 crate::constants::embedding_dim()
676 )));
677 }
678 let mut stmt = conn.prepare_cached(
679 "SELECT entity_id, embedding FROM entity_embeddings WHERE namespace = ?1",
680 )?;
681 let mut scored: Vec<(i64, f32)> = stmt
682 .query_map(params![namespace], |r| {
683 let id: i64 = r.get(0)?;
684 let bytes: Vec<u8> = r.get(1)?;
685 Ok((id, bytes))
686 })?
687 .filter_map(|row| {
688 row.ok().and_then(|(id, bytes)| {
689 let stored = crate::embedder::bytes_to_f32(&bytes);
690 if stored.len() != embedding.len() {
691 return None;
692 }
693 let score = crate::similarity::cosine_similarity(embedding, &stored);
694 Some((id, score))
695 })
696 })
697 .collect();
698 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
701 scored.truncate(k);
702 Ok(scored)
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use crate::constants::embedding_dim;
709 use crate::entity_type::EntityType;
710 use crate::storage::connection::register_vec_extension;
711 use rusqlite::Connection;
712 use tempfile::TempDir;
713
714 type TestResult = Result<(), Box<dyn std::error::Error>>;
715
716 fn setup_db() -> Result<(TempDir, Connection), Box<dyn std::error::Error>> {
717 register_vec_extension();
718 let tmp = TempDir::new()?;
719 let db_path = tmp.path().join("test.db");
720 let mut conn = Connection::open(&db_path)?;
721 crate::migrations::runner().run(&mut conn)?;
722 Ok((tmp, conn))
723 }
724
725 fn insert_memory(conn: &Connection) -> Result<i64, Box<dyn std::error::Error>> {
726 conn.execute(
727 "INSERT INTO memories (namespace, name, type, description, body, body_hash)
728 VALUES ('global', 'test-mem', 'user', 'desc', 'body', 'hash1')",
729 [],
730 )?;
731 Ok(conn.last_insert_rowid())
732 }
733
734 fn new_entity_helper(name: &str) -> NewEntity {
735 NewEntity {
736 name: name.to_string(),
737 entity_type: EntityType::Project,
738 description: None,
739 }
740 }
741
742 fn embedding_zero() -> Vec<f32> {
743 vec![0.0f32; embedding_dim()]
744 }
745
746 #[test]
751 fn test_upsert_entity_creates_new() -> TestResult {
752 let (_tmp, conn) = setup_db()?;
753 let e = new_entity_helper("projeto-alpha");
754 let id = upsert_entity(&conn, "global", &e)?;
755 assert!(id > 0);
756 Ok(())
757 }
758
759 #[test]
760 fn test_upsert_entity_idempotent_returns_same_id() -> TestResult {
761 let (_tmp, conn) = setup_db()?;
762 let e = new_entity_helper("projeto-beta");
763 let id1 = upsert_entity(&conn, "global", &e)?;
764 let id2 = upsert_entity(&conn, "global", &e)?;
765 assert_eq!(id1, id2);
766 Ok(())
767 }
768
769 #[test]
770 fn test_upsert_entity_updates_description() -> TestResult {
771 let (_tmp, conn) = setup_db()?;
772 let e1 = new_entity_helper("projeto-gamma");
773 let id1 = upsert_entity(&conn, "global", &e1)?;
774
775 let e2 = NewEntity {
776 name: "projeto-gamma".to_string(),
777 entity_type: EntityType::Tool,
778 description: Some("nova desc".to_string()),
779 };
780 let id2 = upsert_entity(&conn, "global", &e2)?;
781 assert_eq!(id1, id2);
782
783 let desc: Option<String> = conn.query_row(
784 "SELECT description FROM entities WHERE id = ?1",
785 params![id1],
786 |r| r.get(0),
787 )?;
788 assert_eq!(desc.as_deref(), Some("nova desc"));
789 Ok(())
790 }
791
792 #[test]
793 fn test_upsert_entity_different_namespaces_create_distinct_records() -> TestResult {
794 let (_tmp, conn) = setup_db()?;
795 let e = new_entity_helper("compartilhada");
796 let id1 = upsert_entity(&conn, "ns1", &e)?;
797 let id2 = upsert_entity(&conn, "ns2", &e)?;
798 assert_ne!(id1, id2);
799 Ok(())
800 }
801
802 #[test]
807 #[serial_test::serial(env)]
808 fn test_upsert_entity_vec_first_time_without_conflict() -> TestResult {
809 let (_tmp, conn) = setup_db()?;
810 let e = new_entity_helper("vec-nova");
811 let entity_id = upsert_entity(&conn, "global", &e)?;
812 let emb = embedding_zero();
813
814 let result = upsert_entity_vec(
815 &conn,
816 entity_id,
817 "global",
818 EntityType::Project,
819 &emb,
820 "vec-nova",
821 );
822 assert!(result.is_ok(), "first insertion must succeed");
823
824 let count: i64 = conn.query_row(
825 "SELECT COUNT(*) FROM entity_embeddings WHERE entity_id = ?1",
826 params![entity_id],
827 |r| r.get(0),
828 )?;
829 assert_eq!(count, 1, "must have exactly one row after insertion");
830 Ok(())
831 }
832
833 #[test]
834 #[serial_test::serial(env)]
835 fn test_upsert_entity_vec_second_time_replaces_without_error() -> TestResult {
836 let (_tmp, conn) = setup_db()?;
838 let e = new_entity_helper("vec-existente");
839 let entity_id = upsert_entity(&conn, "global", &e)?;
840 let emb = embedding_zero();
841
842 upsert_entity_vec(
843 &conn,
844 entity_id,
845 "global",
846 EntityType::Project,
847 &emb,
848 "vec-existente",
849 )?;
850
851 let result = upsert_entity_vec(
853 &conn,
854 entity_id,
855 "global",
856 EntityType::Tool,
857 &emb,
858 "vec-existente",
859 );
860 assert!(
861 result.is_ok(),
862 "second insertion (replace) must succeed: {result:?}"
863 );
864
865 let count: i64 = conn.query_row(
866 "SELECT COUNT(*) FROM entity_embeddings WHERE entity_id = ?1",
867 params![entity_id],
868 |r| r.get(0),
869 )?;
870 assert_eq!(count, 1, "must have exactly one row after replacement");
871 Ok(())
872 }
873
874 #[test]
875 #[serial_test::serial(env)]
876 fn test_upsert_entity_vec_multiple_independent_entities() -> TestResult {
877 let (_tmp, conn) = setup_db()?;
878 let emb = embedding_zero();
879
880 for i in 0..3i64 {
881 let nome = format!("ent-{i}");
882 let e = new_entity_helper(&nome);
883 let entity_id = upsert_entity(&conn, "global", &e)?;
884 upsert_entity_vec(&conn, entity_id, "global", EntityType::Project, &emb, &nome)?;
885 }
886
887 let count: i64 =
888 conn.query_row("SELECT COUNT(*) FROM entity_embeddings", [], |r| r.get(0))?;
889 assert_eq!(
890 count, 3,
891 "must have three distinct rows in entity_embeddings"
892 );
893 Ok(())
894 }
895
896 #[test]
901 fn test_find_entity_id_existing_returns_some() -> TestResult {
902 let (_tmp, conn) = setup_db()?;
903 let e = new_entity_helper("entidade-busca");
904 let id_inserido = upsert_entity(&conn, "global", &e)?;
905 let id_encontrado = find_entity_id(&conn, "global", "entidade-busca")?;
906 assert_eq!(id_encontrado, Some(id_inserido));
907 Ok(())
908 }
909
910 #[test]
911 fn test_find_entity_id_missing_returns_none() -> TestResult {
912 let (_tmp, conn) = setup_db()?;
913 let id = find_entity_id(&conn, "global", "nao-existe")?;
914 assert_eq!(id, None);
915 Ok(())
916 }
917
918 #[test]
923 fn test_delete_entities_by_ids_empty_list_returns_zero() -> TestResult {
924 let (_tmp, conn) = setup_db()?;
925 let removed = delete_entities_by_ids(&conn, &[])?;
926 assert_eq!(removed, 0);
927 Ok(())
928 }
929
930 #[test]
931 fn test_delete_entities_by_ids_removes_valid_entity() -> TestResult {
932 let (_tmp, conn) = setup_db()?;
933 let e = new_entity_helper("to-delete");
934 let entity_id = upsert_entity(&conn, "global", &e)?;
935
936 let removed = delete_entities_by_ids(&conn, &[entity_id])?;
937 assert_eq!(removed, 1);
938
939 let id = find_entity_id(&conn, "global", "to-delete")?;
940 assert_eq!(id, None, "entity must have been removed");
941 Ok(())
942 }
943
944 #[test]
945 fn test_delete_entities_by_ids_missing_id_returns_zero() -> TestResult {
946 let (_tmp, conn) = setup_db()?;
947 let removed = delete_entities_by_ids(&conn, &[9999])?;
948 assert_eq!(removed, 0);
949 Ok(())
950 }
951
952 #[test]
953 fn test_delete_entities_by_ids_removes_multiple() -> TestResult {
954 let (_tmp, conn) = setup_db()?;
955 let id1 = upsert_entity(&conn, "global", &new_entity_helper("del-a"))?;
956 let id2 = upsert_entity(&conn, "global", &new_entity_helper("del-b"))?;
957 let id3 = upsert_entity(&conn, "global", &new_entity_helper("del-c"))?;
958
959 let removed = delete_entities_by_ids(&conn, &[id1, id2])?;
960 assert_eq!(removed, 2);
961
962 assert!(find_entity_id(&conn, "global", "del-a")?.is_none());
963 assert!(find_entity_id(&conn, "global", "del-b")?.is_none());
964 assert!(find_entity_id(&conn, "global", "del-c")?.is_some());
965 let _ = id3;
966 Ok(())
967 }
968
969 #[test]
970 fn test_delete_entities_by_ids_also_removes_vec() -> TestResult {
971 let (_tmp, conn) = setup_db()?;
972 let e = new_entity_helper("del-com-vec");
973 let entity_id = upsert_entity(&conn, "global", &e)?;
974 let emb = embedding_zero();
975 upsert_entity_vec(
976 &conn,
977 entity_id,
978 "global",
979 EntityType::Project,
980 &emb,
981 "del-com-vec",
982 )?;
983
984 let count_antes: i64 = conn.query_row(
985 "SELECT COUNT(*) FROM entity_embeddings WHERE entity_id = ?1",
986 params![entity_id],
987 |r| r.get(0),
988 )?;
989 assert_eq!(count_antes, 1);
990
991 delete_entities_by_ids(&conn, &[entity_id])?;
992
993 let count_depois: i64 = conn.query_row(
994 "SELECT COUNT(*) FROM entity_embeddings WHERE entity_id = ?1",
995 params![entity_id],
996 |r| r.get(0),
997 )?;
998 assert_eq!(
999 count_depois, 0,
1000 "entity_embeddings deve ser limpo junto com entities"
1001 );
1002 Ok(())
1003 }
1004
1005 #[test]
1010 fn test_upsert_relationship_creates_new() -> TestResult {
1011 let (_tmp, conn) = setup_db()?;
1012 let id_a = upsert_entity(&conn, "global", &new_entity_helper("rel-a"))?;
1013 let id_b = upsert_entity(&conn, "global", &new_entity_helper("rel-b"))?;
1014
1015 let rel = NewRelationship {
1016 source: "rel-a".to_string(),
1017 target: "rel-b".to_string(),
1018 relation: "uses".to_string(),
1019 strength: 0.8,
1020 description: None,
1021 };
1022 let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
1023 assert!(rel_id > 0);
1024 Ok(())
1025 }
1026
1027 #[test]
1028 fn test_upsert_relationship_idempotent() -> TestResult {
1029 let (_tmp, conn) = setup_db()?;
1030 let id_a = upsert_entity(&conn, "global", &new_entity_helper("idem-a"))?;
1031 let id_b = upsert_entity(&conn, "global", &new_entity_helper("idem-b"))?;
1032
1033 let rel = NewRelationship {
1034 source: "idem-a".to_string(),
1035 target: "idem-b".to_string(),
1036 relation: "uses".to_string(),
1037 strength: 0.5,
1038 description: None,
1039 };
1040 let id1 = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
1041 let id2 = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
1042 assert_eq!(id1, id2);
1043 Ok(())
1044 }
1045
1046 #[test]
1047 fn test_find_relationship_existing() -> TestResult {
1048 let (_tmp, conn) = setup_db()?;
1049 let id_a = upsert_entity(&conn, "global", &new_entity_helper("fr-a"))?;
1050 let id_b = upsert_entity(&conn, "global", &new_entity_helper("fr-b"))?;
1051
1052 let rel = NewRelationship {
1053 source: "fr-a".to_string(),
1054 target: "fr-b".to_string(),
1055 relation: "depends_on".to_string(),
1056 strength: 0.7,
1057 description: None,
1058 };
1059 upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
1060
1061 let encontrada = find_relationship(&conn, id_a, id_b, "depends_on")?;
1062 let row = encontrada.ok_or("relationship should exist")?;
1063 assert_eq!(row.source_id, id_a);
1064 assert_eq!(row.target_id, id_b);
1065 assert!((row.weight - 0.7).abs() < 1e-9);
1066 Ok(())
1067 }
1068
1069 #[test]
1070 fn test_find_relationship_missing_returns_none() -> TestResult {
1071 let (_tmp, conn) = setup_db()?;
1072 let resultado = find_relationship(&conn, 9999, 8888, "uses")?;
1073 assert!(resultado.is_none());
1074 Ok(())
1075 }
1076
1077 #[test]
1082 fn test_link_memory_entity_idempotent() -> TestResult {
1083 let (_tmp, conn) = setup_db()?;
1084 let memory_id = insert_memory(&conn)?;
1085 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("me-ent"))?;
1086
1087 link_memory_entity(&conn, memory_id, entity_id)?;
1088 let resultado = link_memory_entity(&conn, memory_id, entity_id);
1089 assert!(
1090 resultado.is_ok(),
1091 "INSERT OR IGNORE must not fail on duplicate"
1092 );
1093 Ok(())
1094 }
1095
1096 #[test]
1097 fn test_link_memory_relationship_idempotent() -> TestResult {
1098 let (_tmp, conn) = setup_db()?;
1099 let memory_id = insert_memory(&conn)?;
1100 let id_a = upsert_entity(&conn, "global", &new_entity_helper("mr-a"))?;
1101 let id_b = upsert_entity(&conn, "global", &new_entity_helper("mr-b"))?;
1102
1103 let rel = NewRelationship {
1104 source: "mr-a".to_string(),
1105 target: "mr-b".to_string(),
1106 relation: "uses".to_string(),
1107 strength: 0.5,
1108 description: None,
1109 };
1110 let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
1111
1112 link_memory_relationship(&conn, memory_id, rel_id)?;
1113 let resultado = link_memory_relationship(&conn, memory_id, rel_id);
1114 assert!(
1115 resultado.is_ok(),
1116 "INSERT OR IGNORE must not fail on duplicate"
1117 );
1118 Ok(())
1119 }
1120
1121 #[test]
1126 fn test_increment_degree_increases_counter() -> TestResult {
1127 let (_tmp, conn) = setup_db()?;
1128 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("grau-ent"))?;
1129
1130 increment_degree(&conn, entity_id)?;
1131 increment_degree(&conn, entity_id)?;
1132
1133 let degree: i64 = conn.query_row(
1134 "SELECT degree FROM entities WHERE id = ?1",
1135 params![entity_id],
1136 |r| r.get(0),
1137 )?;
1138 assert_eq!(degree, 2);
1139 Ok(())
1140 }
1141
1142 #[test]
1143 fn test_recalculate_degree_reflects_actual_relations() -> TestResult {
1144 let (_tmp, conn) = setup_db()?;
1145 let id_a = upsert_entity(&conn, "global", &new_entity_helper("rc-a"))?;
1146 let id_b = upsert_entity(&conn, "global", &new_entity_helper("rc-b"))?;
1147 let id_c = upsert_entity(&conn, "global", &new_entity_helper("rc-c"))?;
1148
1149 let rel1 = NewRelationship {
1150 source: "rc-a".to_string(),
1151 target: "rc-b".to_string(),
1152 relation: "uses".to_string(),
1153 strength: 0.5,
1154 description: None,
1155 };
1156 let rel2 = NewRelationship {
1157 source: "rc-c".to_string(),
1158 target: "rc-a".to_string(),
1159 relation: "depends_on".to_string(),
1160 strength: 0.5,
1161 description: None,
1162 };
1163 upsert_relationship(&conn, "global", id_a, id_b, &rel1)?;
1164 upsert_relationship(&conn, "global", id_c, id_a, &rel2)?;
1165
1166 recalculate_degree(&conn, id_a)?;
1167
1168 let degree: i64 = conn.query_row(
1169 "SELECT degree FROM entities WHERE id = ?1",
1170 params![id_a],
1171 |r| r.get(0),
1172 )?;
1173 assert_eq!(
1174 degree, 2,
1175 "rc-a appears in two relationships (source+target)"
1176 );
1177 Ok(())
1178 }
1179
1180 #[test]
1185 fn test_find_orphan_entity_ids_without_orphans() -> TestResult {
1186 let (_tmp, conn) = setup_db()?;
1187 let memory_id = insert_memory(&conn)?;
1188 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("nao-orfa"))?;
1189 link_memory_entity(&conn, memory_id, entity_id)?;
1190
1191 let orfas = find_orphan_entity_ids(&conn, Some("global"))?;
1192 assert!(!orfas.contains(&entity_id));
1193 Ok(())
1194 }
1195
1196 #[test]
1197 fn test_find_orphan_entity_ids_detects_orphans() -> TestResult {
1198 let (_tmp, conn) = setup_db()?;
1199 let entity_id = upsert_entity(&conn, "global", &new_entity_helper("sim-orfa"))?;
1200
1201 let orfas = find_orphan_entity_ids(&conn, Some("global"))?;
1202 assert!(orfas.contains(&entity_id));
1203 Ok(())
1204 }
1205
1206 #[test]
1207 fn test_find_orphan_entity_ids_without_namespace_returns_all() -> TestResult {
1208 let (_tmp, conn) = setup_db()?;
1209 let id1 = upsert_entity(&conn, "ns-a", &new_entity_helper("orfa-a"))?;
1210 let id2 = upsert_entity(&conn, "ns-b", &new_entity_helper("orfa-b"))?;
1211
1212 let orfas = find_orphan_entity_ids(&conn, None)?;
1213 assert!(orfas.contains(&id1));
1214 assert!(orfas.contains(&id2));
1215 Ok(())
1216 }
1217
1218 #[test]
1223 fn test_list_entities_with_namespace() -> TestResult {
1224 let (_tmp, conn) = setup_db()?;
1225 upsert_entity(&conn, "le-ns", &new_entity_helper("le-ent-1"))?;
1226 upsert_entity(&conn, "le-ns", &new_entity_helper("le-ent-2"))?;
1227 upsert_entity(&conn, "outro-ns", &new_entity_helper("le-ent-3"))?;
1228
1229 let lista = list_entities(&conn, Some("le-ns"))?;
1230 assert_eq!(lista.len(), 2);
1231 assert!(lista.iter().all(|e| e.namespace == "le-ns"));
1232 Ok(())
1233 }
1234
1235 #[test]
1236 fn test_list_entities_without_namespace_returns_all() -> TestResult {
1237 let (_tmp, conn) = setup_db()?;
1238 upsert_entity(&conn, "ns1", &new_entity_helper("all-ent-1"))?;
1239 upsert_entity(&conn, "ns2", &new_entity_helper("all-ent-2"))?;
1240
1241 let lista = list_entities(&conn, None)?;
1242 assert!(lista.len() >= 2);
1243 Ok(())
1244 }
1245
1246 #[test]
1247 fn test_list_relationships_by_namespace_filters_correctly() -> TestResult {
1248 let (_tmp, conn) = setup_db()?;
1249 let id_a = upsert_entity(&conn, "rel-ns", &new_entity_helper("lr-a"))?;
1250 let id_b = upsert_entity(&conn, "rel-ns", &new_entity_helper("lr-b"))?;
1251
1252 let rel = NewRelationship {
1253 source: "lr-a".to_string(),
1254 target: "lr-b".to_string(),
1255 relation: "uses".to_string(),
1256 strength: 0.5,
1257 description: None,
1258 };
1259 upsert_relationship(&conn, "rel-ns", id_a, id_b, &rel)?;
1260
1261 let lista = list_relationships_by_namespace(&conn, Some("rel-ns"))?;
1262 assert!(!lista.is_empty());
1263 assert!(lista.iter().all(|r| r.namespace == "rel-ns"));
1264 Ok(())
1265 }
1266
1267 #[test]
1272 fn test_delete_relationship_by_id_removes_relation() -> TestResult {
1273 let (_tmp, conn) = setup_db()?;
1274 let id_a = upsert_entity(&conn, "global", &new_entity_helper("dr-a"))?;
1275 let id_b = upsert_entity(&conn, "global", &new_entity_helper("dr-b"))?;
1276
1277 let rel = NewRelationship {
1278 source: "dr-a".to_string(),
1279 target: "dr-b".to_string(),
1280 relation: "uses".to_string(),
1281 strength: 0.5,
1282 description: None,
1283 };
1284 let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
1285
1286 delete_relationship_by_id(&conn, rel_id)?;
1287
1288 let encontrada = find_relationship(&conn, id_a, id_b, "uses")?;
1289 assert!(encontrada.is_none(), "relationship must have been removed");
1290 Ok(())
1291 }
1292
1293 #[test]
1294 fn test_create_or_fetch_relationship_creates_new() -> TestResult {
1295 let (_tmp, conn) = setup_db()?;
1296 let id_a = upsert_entity(&conn, "global", &new_entity_helper("cf-a"))?;
1297 let id_b = upsert_entity(&conn, "global", &new_entity_helper("cf-b"))?;
1298
1299 let (rel_id, created) =
1300 create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1301 assert!(rel_id > 0);
1302 assert!(created);
1303 Ok(())
1304 }
1305
1306 #[test]
1307 fn test_create_or_fetch_relationship_returns_existing() -> TestResult {
1308 let (_tmp, conn) = setup_db()?;
1309 let id_a = upsert_entity(&conn, "global", &new_entity_helper("cf2-a"))?;
1310 let id_b = upsert_entity(&conn, "global", &new_entity_helper("cf2-b"))?;
1311
1312 create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1313 let (_, created) =
1314 create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1315 assert!(
1316 !created,
1317 "second call must return the existing relationship"
1318 );
1319 Ok(())
1320 }
1321
1322 #[test]
1327 fn accepts_type_field_as_alias() -> TestResult {
1328 let json = r#"{"name": "X", "type": "concept"}"#;
1329 let ent: NewEntity = serde_json::from_str(json)?;
1330 assert_eq!(ent.entity_type, EntityType::Concept);
1331 Ok(())
1332 }
1333
1334 #[test]
1335 fn accepts_canonical_entity_type_field() -> TestResult {
1336 let json = r#"{"name": "X", "entity_type": "concept"}"#;
1337 let ent: NewEntity = serde_json::from_str(json)?;
1338 assert_eq!(ent.entity_type, EntityType::Concept);
1339 Ok(())
1340 }
1341
1342 #[test]
1343 fn both_fields_present_yields_duplicate_error() {
1344 let json = r#"{"name": "X", "entity_type": "concept", "type": "person"}"#;
1346 let resultado: Result<NewEntity, _> = serde_json::from_str(json);
1347 assert!(
1348 resultado.is_err(),
1349 "both fields in the same JSON are a duplicate"
1350 );
1351 }
1352
1353 #[test]
1354 fn validate_entity_name_accepts_valid() {
1355 assert!(validate_entity_name("rust-lang").is_ok());
1356 assert!(validate_entity_name("sqlite-graphrag").is_ok());
1357 assert!(validate_entity_name("ab").is_ok());
1358 }
1359
1360 #[test]
1361 fn validate_entity_name_rejects_short() {
1362 assert!(validate_entity_name("a").is_err());
1363 assert!(validate_entity_name("").is_err());
1364 }
1365
1366 #[test]
1367 fn validate_entity_name_rejects_newlines() {
1368 assert!(validate_entity_name("foo\nbar").is_err());
1369 assert!(validate_entity_name("foo\rbar").is_err());
1370 }
1371
1372 #[test]
1373 fn validate_entity_name_rejects_short_allcaps() {
1374 assert!(validate_entity_name("RAM").is_err());
1375 assert!(validate_entity_name("NAO").is_err());
1376 assert!(validate_entity_name("OK").is_err());
1377 }
1378
1379 #[test]
1380 fn validate_entity_name_accepts_long_allcaps() {
1381 assert!(validate_entity_name("SQLITE").is_ok());
1382 assert!(validate_entity_name("GRAPHRAG").is_ok());
1383 }
1384
1385 #[test]
1386 fn validate_entity_name_accepts_mixed_case() {
1387 assert!(validate_entity_name("FTS5").is_ok()); assert!(validate_entity_name("WAL").is_err()); }
1390}