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