Skip to main content

sqlite_graphrag/storage/
entities.rs

1//! Persistence layer for entities, relationships and their junction tables.
2//!
3//! The entity graph mirrors the conceptual content of memories: `entities`
4//! holds nodes, `relationships` holds typed edges and `memory_entities` and
5//! `memory_relationships` connect each memory to the graph slice it emitted.
6
7use crate::embedder::f32_to_bytes;
8use crate::errors::AppError;
9use rusqlite::{params, Connection};
10use serde::{Deserialize, Serialize};
11
12/// Input payload used to upsert a single entity.
13///
14/// `name` is normalized to kebab-case by the caller. `description` is
15/// optional and preserved across upserts when the new value is `None`.
16#[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/// Input payload used to upsert a typed relationship between entities.
26///
27/// `strength` must lie within `[0.0, 1.0]` and is mapped to the `weight`
28/// column of the `relationships` table.
29#[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
41/// Upserts an entity and returns its primary key.
42///
43/// Uses `ON CONFLICT(namespace, name)` to keep one row per entity within a
44/// namespace, refreshing `type` and `description` opportunistically.
45///
46/// # Errors
47///
48/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
49pub 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
67/// Replaces the vector row for an entity in `vec_entities`.
68///
69/// vec0 virtual tables do not honour `INSERT OR REPLACE` when the primary key
70/// already exists — they raise a UNIQUE constraint error instead of silently
71/// replacing the row. The workaround is an explicit DELETE before INSERT so
72/// that the insert never conflicts. `embedding` must have length
73/// [`crate::constants::EMBEDDING_DIM`].
74///
75/// # Errors
76///
77/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
78pub 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
104/// Upserts a typed relationship between two entity ids.
105///
106/// Conflicts on `(source_id, target_id, relation)` refresh `weight` and
107/// preserve a non-null `description`. Returns the `rowid` of the stored row.
108///
109/// # Errors
110///
111/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
112pub 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
174/// Busca a entidade por nome e namespace. Retorna o id se existir.
175pub 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/// Estrutura representando uma relação existente.
190#[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
201/// Busca uma relação específica por (source_id, target_id, relation).
202pub 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
230/// Cria uma relação se não existir (retorna action="created")
231/// ou retorna a relação existente (action="already_exists") com peso atualizado.
232pub 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    // Check if it exists first.
242    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
266/// Remove uma relação pelo id e limpa memory_relationships.
267pub 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
279/// Recalcula o campo `degree` de uma entidade.
280pub 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/// Linha de entidade com dados suficientes para exportação/consulta de grafo.
292#[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
300/// Lista entidades, filtrando por namespace se fornecido.
301pub 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
337/// Lista relações filtradas por namespace (das entidades de origem/destino).
338pub 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
386/// Localiza entidades órfãs: sem vínculo em memory_entities e sem relações.
387pub 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
421/// Deleta entidades e seus vetores associados. Retorna o número de entidades removidas.
422pub 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        // vec0 lacks FK CASCADE — clean vec_entities explicitly.
429        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    // ------------------------------------------------------------------ //
497    // upsert_entity
498    // ------------------------------------------------------------------ //
499
500    #[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    // ------------------------------------------------------------------ //
553    // upsert_entity_vec — cobre DELETE+INSERT (branch novo após fix OOM)
554    // ------------------------------------------------------------------ //
555
556    #[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        // Cobre o branch onde DELETE remove a linha existente antes do INSERT.
578        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        // Segunda chamada: DELETE retorna 1 linha removida, INSERT deve ter sucesso.
586        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    // ------------------------------------------------------------------ //
623    // find_entity_id
624    // ------------------------------------------------------------------ //
625
626    #[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    // ------------------------------------------------------------------ //
645    // delete_entities_by_ids
646    // ------------------------------------------------------------------ //
647
648    #[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    // ------------------------------------------------------------------ //
725    // upsert_relationship / find_relationship
726    // ------------------------------------------------------------------ //
727
728    #[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    // ------------------------------------------------------------------ //
797    // link_memory_entity / link_memory_relationship
798    // ------------------------------------------------------------------ //
799
800    #[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    // ------------------------------------------------------------------ //
841    // increment_degree / recalculate_degree
842    // ------------------------------------------------------------------ //
843
844    #[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    // ------------------------------------------------------------------ //
897    // find_orphan_entity_ids
898    // ------------------------------------------------------------------ //
899
900    #[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    // ------------------------------------------------------------------ //
935    // list_entities / list_relationships_by_namespace
936    // ------------------------------------------------------------------ //
937
938    #[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    // ------------------------------------------------------------------ //
984    // delete_relationship_by_id / create_or_fetch_relationship
985    // ------------------------------------------------------------------ //
986
987    #[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    // ------------------------------------------------------------------ //
1036    // serde alias: campo "type" aceito como sinônimo de "entity_type"
1037    // ------------------------------------------------------------------ //
1038
1039    #[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        // serde trata alias como nome alternativo do mesmo campo;
1058        // ter entity_type e type no mesmo JSON é duplicata e deve falhar
1059        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}