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::entity_type::EntityType;
9use crate::errors::AppError;
10use crate::storage::utils::with_busy_retry;
11use rusqlite::{params, Connection};
12use serde::{Deserialize, Serialize};
13
14/// Input payload used to upsert a single entity.
15///
16/// `name` is normalized to kebab-case by the caller. `description` is
17/// optional and preserved across upserts when the new value is `None`.
18#[derive(Debug, Serialize, Deserialize, Clone)]
19#[serde(deny_unknown_fields)]
20pub struct NewEntity {
21    pub name: String,
22    #[serde(alias = "type")]
23    pub entity_type: EntityType,
24    pub description: Option<String>,
25}
26
27/// Input payload used to upsert a typed relationship between entities.
28///
29/// `strength` must lie within `[0.0, 1.0]` and is mapped to the `weight`
30/// column of the `relationships` table.
31#[derive(Debug, Serialize, Deserialize, Clone)]
32#[serde(deny_unknown_fields)]
33pub struct NewRelationship {
34    #[serde(alias = "from")]
35    pub source: String,
36    #[serde(alias = "to")]
37    pub target: String,
38    pub relation: String,
39    pub strength: f64,
40    pub description: Option<String>,
41}
42
43/// Upserts an entity and returns its primary key.
44///
45/// Uses `ON CONFLICT(namespace, name)` to keep one row per entity within a
46/// namespace, refreshing `type` and `description` opportunistically.
47///
48/// # Errors
49///
50/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
51pub fn upsert_entity(conn: &Connection, namespace: &str, e: &NewEntity) -> Result<i64, AppError> {
52    conn.execute(
53        "INSERT INTO entities (namespace, name, type, description)
54         VALUES (?1, ?2, ?3, ?4)
55         ON CONFLICT(namespace, name) DO UPDATE SET
56           type        = excluded.type,
57           description = COALESCE(excluded.description, entities.description),
58           updated_at  = unixepoch()",
59        params![namespace, e.name, e.entity_type, e.description],
60    )?;
61    let id: i64 = conn.query_row(
62        "SELECT id FROM entities WHERE namespace = ?1 AND name = ?2",
63        params![namespace, e.name],
64        |r| r.get(0),
65    )?;
66    Ok(id)
67}
68
69/// Replaces the vector row for an entity in `vec_entities`.
70///
71/// vec0 virtual tables do not honour `INSERT OR REPLACE` when the primary key
72/// already exists — they raise a UNIQUE constraint error instead of silently
73/// replacing the row. The workaround is an explicit DELETE before INSERT so
74/// that the insert never conflicts. `embedding` must have length
75/// [`crate::constants::EMBEDDING_DIM`].
76///
77/// # Errors
78///
79/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
80pub fn upsert_entity_vec(
81    conn: &Connection,
82    entity_id: i64,
83    namespace: &str,
84    entity_type: EntityType,
85    embedding: &[f32],
86    name: &str,
87) -> Result<(), AppError> {
88    // Both statements wrapped in with_busy_retry: WAL concurrency can cause
89    // SQLITE_BUSY on vec0 virtual table writes when multiple CLI instances run.
90    let embedding_bytes = f32_to_bytes(embedding);
91    with_busy_retry(|| {
92        conn.execute(
93            "DELETE FROM vec_entities WHERE entity_id = ?1",
94            params![entity_id],
95        )?;
96        conn.execute(
97            "INSERT INTO vec_entities(entity_id, namespace, type, embedding, name)
98             VALUES (?1, ?2, ?3, ?4, ?5)",
99            params![entity_id, namespace, entity_type, &embedding_bytes, name],
100        )?;
101        Ok(())
102    })
103}
104
105/// Upserts a typed relationship between two entity ids.
106///
107/// Conflicts on `(source_id, target_id, relation)` refresh `weight` and
108/// preserve a non-null `description`. Returns the `rowid` of the stored row.
109///
110/// # Errors
111///
112/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
113pub fn upsert_relationship(
114    conn: &Connection,
115    namespace: &str,
116    source_id: i64,
117    target_id: i64,
118    rel: &NewRelationship,
119) -> Result<i64, AppError> {
120    conn.execute(
121        "INSERT INTO relationships (namespace, source_id, target_id, relation, weight, description)
122         VALUES (?1, ?2, ?3, ?4, ?5, ?6)
123         ON CONFLICT(source_id, target_id, relation) DO UPDATE SET
124           weight = excluded.weight,
125           description = COALESCE(excluded.description, relationships.description)",
126        params![
127            namespace,
128            source_id,
129            target_id,
130            rel.relation,
131            rel.strength,
132            rel.description
133        ],
134    )?;
135    let id: i64 = conn.query_row(
136        "SELECT id FROM relationships WHERE source_id=?1 AND target_id=?2 AND relation=?3",
137        params![source_id, target_id, rel.relation],
138        |r| r.get(0),
139    )?;
140    Ok(id)
141}
142
143pub fn link_memory_entity(
144    conn: &Connection,
145    memory_id: i64,
146    entity_id: i64,
147) -> Result<(), AppError> {
148    conn.execute(
149        "INSERT OR IGNORE INTO memory_entities (memory_id, entity_id) VALUES (?1, ?2)",
150        params![memory_id, entity_id],
151    )?;
152    Ok(())
153}
154
155pub fn link_memory_relationship(
156    conn: &Connection,
157    memory_id: i64,
158    rel_id: i64,
159) -> Result<(), AppError> {
160    conn.execute(
161        "INSERT OR IGNORE INTO memory_relationships (memory_id, relationship_id) VALUES (?1, ?2)",
162        params![memory_id, rel_id],
163    )?;
164    Ok(())
165}
166
167pub fn increment_degree(conn: &Connection, entity_id: i64) -> Result<(), AppError> {
168    conn.execute(
169        "UPDATE entities SET degree = degree + 1 WHERE id = ?1",
170        params![entity_id],
171    )?;
172    Ok(())
173}
174
175/// Looks up the entity by name and namespace. Returns the id when it exists.
176pub fn find_entity_id(
177    conn: &Connection,
178    namespace: &str,
179    name: &str,
180) -> Result<Option<i64>, AppError> {
181    let mut stmt =
182        conn.prepare_cached("SELECT id FROM entities WHERE namespace = ?1 AND name = ?2")?;
183    match stmt.query_row(params![namespace, name], |r| r.get::<_, i64>(0)) {
184        Ok(id) => Ok(Some(id)),
185        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
186        Err(e) => Err(AppError::Database(e)),
187    }
188}
189
190/// Structure representing an existing relation.
191#[derive(Debug, Serialize)]
192pub struct RelationshipRow {
193    pub id: i64,
194    pub namespace: String,
195    pub source_id: i64,
196    pub target_id: i64,
197    pub relation: String,
198    pub weight: f64,
199    pub description: Option<String>,
200}
201
202/// Looks up a specific relation by (source_id, target_id, relation).
203pub fn find_relationship(
204    conn: &Connection,
205    source_id: i64,
206    target_id: i64,
207    relation: &str,
208) -> Result<Option<RelationshipRow>, AppError> {
209    let mut stmt = conn.prepare_cached(
210        "SELECT id, namespace, source_id, target_id, relation, weight, description
211         FROM relationships
212         WHERE source_id = ?1 AND target_id = ?2 AND relation = ?3",
213    )?;
214    match stmt.query_row(params![source_id, target_id, relation], |r| {
215        Ok(RelationshipRow {
216            id: r.get(0)?,
217            namespace: r.get(1)?,
218            source_id: r.get(2)?,
219            target_id: r.get(3)?,
220            relation: r.get(4)?,
221            weight: r.get(5)?,
222            description: r.get(6)?,
223        })
224    }) {
225        Ok(row) => Ok(Some(row)),
226        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
227        Err(e) => Err(AppError::Database(e)),
228    }
229}
230
231/// Creates a relation if it does not exist (returns action="created")
232/// or returns the existing relation (action="already_exists") with updated weight.
233pub fn create_or_fetch_relationship(
234    conn: &Connection,
235    namespace: &str,
236    source_id: i64,
237    target_id: i64,
238    relation: &str,
239    weight: f64,
240    description: Option<&str>,
241) -> Result<(i64, bool), AppError> {
242    // Check if it exists first.
243    let existing = find_relationship(conn, source_id, target_id, relation)?;
244    if let Some(row) = existing {
245        return Ok((row.id, false));
246    }
247    conn.execute(
248        "INSERT INTO relationships (namespace, source_id, target_id, relation, weight, description)
249         VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
250        params![
251            namespace,
252            source_id,
253            target_id,
254            relation,
255            weight,
256            description
257        ],
258    )?;
259    let id: i64 = conn.query_row(
260        "SELECT id FROM relationships WHERE source_id = ?1 AND target_id = ?2 AND relation = ?3",
261        params![source_id, target_id, relation],
262        |r| r.get(0),
263    )?;
264    Ok((id, true))
265}
266
267/// Removes a relation by id and cleans up memory_relationships.
268pub fn delete_relationship_by_id(conn: &Connection, relationship_id: i64) -> Result<(), AppError> {
269    conn.execute(
270        "DELETE FROM memory_relationships WHERE relationship_id = ?1",
271        params![relationship_id],
272    )?;
273    conn.execute(
274        "DELETE FROM relationships WHERE id = ?1",
275        params![relationship_id],
276    )?;
277    Ok(())
278}
279
280/// Recalculates the `degree` field of an entity.
281pub fn recalculate_degree(conn: &Connection, entity_id: i64) -> Result<(), AppError> {
282    conn.execute(
283        "UPDATE entities
284         SET degree = (SELECT COUNT(*) FROM relationships
285                       WHERE source_id = entities.id OR target_id = entities.id)
286         WHERE id = ?1",
287        params![entity_id],
288    )?;
289    Ok(())
290}
291
292/// Entity row with enough data for graph export/query.
293#[derive(Debug, Serialize, Clone)]
294pub struct EntityNode {
295    pub id: i64,
296    pub name: String,
297    pub namespace: String,
298    pub kind: String,
299}
300
301/// Lists entities, filtering by namespace if provided.
302pub fn list_entities(
303    conn: &Connection,
304    namespace: Option<&str>,
305) -> Result<Vec<EntityNode>, AppError> {
306    if let Some(ns) = namespace {
307        let mut stmt = conn.prepare(
308            "SELECT id, name, namespace, type FROM entities WHERE namespace = ?1 ORDER BY id",
309        )?;
310        let rows = stmt
311            .query_map(params![ns], |r| {
312                Ok(EntityNode {
313                    id: r.get(0)?,
314                    name: r.get(1)?,
315                    namespace: r.get(2)?,
316                    kind: r.get(3)?,
317                })
318            })?
319            .collect::<Result<Vec<_>, _>>()?;
320        Ok(rows)
321    } else {
322        let mut stmt =
323            conn.prepare("SELECT id, name, namespace, type FROM entities ORDER BY namespace, id")?;
324        let rows = stmt
325            .query_map([], |r| {
326                Ok(EntityNode {
327                    id: r.get(0)?,
328                    name: r.get(1)?,
329                    namespace: r.get(2)?,
330                    kind: r.get(3)?,
331                })
332            })?
333            .collect::<Result<Vec<_>, _>>()?;
334        Ok(rows)
335    }
336}
337
338/// Lists relations filtered by namespace (of source/target entities).
339pub fn list_relationships_by_namespace(
340    conn: &Connection,
341    namespace: Option<&str>,
342) -> Result<Vec<RelationshipRow>, AppError> {
343    if let Some(ns) = namespace {
344        let mut stmt = conn.prepare(
345            "SELECT r.id, r.namespace, r.source_id, r.target_id, r.relation, r.weight, r.description
346             FROM relationships r
347             JOIN entities se ON se.id = r.source_id AND se.namespace = ?1
348             JOIN entities te ON te.id = r.target_id AND te.namespace = ?1
349             ORDER BY r.id",
350        )?;
351        let rows = stmt
352            .query_map(params![ns], |r| {
353                Ok(RelationshipRow {
354                    id: r.get(0)?,
355                    namespace: r.get(1)?,
356                    source_id: r.get(2)?,
357                    target_id: r.get(3)?,
358                    relation: r.get(4)?,
359                    weight: r.get(5)?,
360                    description: r.get(6)?,
361                })
362            })?
363            .collect::<Result<Vec<_>, _>>()?;
364        Ok(rows)
365    } else {
366        let mut stmt = conn.prepare(
367            "SELECT id, namespace, source_id, target_id, relation, weight, description
368             FROM relationships ORDER BY id",
369        )?;
370        let rows = stmt
371            .query_map([], |r| {
372                Ok(RelationshipRow {
373                    id: r.get(0)?,
374                    namespace: r.get(1)?,
375                    source_id: r.get(2)?,
376                    target_id: r.get(3)?,
377                    relation: r.get(4)?,
378                    weight: r.get(5)?,
379                    description: r.get(6)?,
380                })
381            })?
382            .collect::<Result<Vec<_>, _>>()?;
383        Ok(rows)
384    }
385}
386
387/// Locates orphan entities: no link in memory_entities and no relations.
388pub fn find_orphan_entity_ids(
389    conn: &Connection,
390    namespace: Option<&str>,
391) -> Result<Vec<i64>, AppError> {
392    if let Some(ns) = namespace {
393        let mut stmt = conn.prepare(
394            "SELECT e.id FROM entities e
395             WHERE e.namespace = ?1
396               AND NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
397               AND NOT EXISTS (
398                   SELECT 1 FROM relationships r
399                   WHERE r.source_id = e.id OR r.target_id = e.id
400               )",
401        )?;
402        let ids = stmt
403            .query_map(params![ns], |r| r.get::<_, i64>(0))?
404            .collect::<Result<Vec<_>, _>>()?;
405        Ok(ids)
406    } else {
407        let mut stmt = conn.prepare(
408            "SELECT e.id FROM entities e
409             WHERE NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
410               AND NOT EXISTS (
411                   SELECT 1 FROM relationships r
412                   WHERE r.source_id = e.id OR r.target_id = e.id
413               )",
414        )?;
415        let ids = stmt
416            .query_map([], |r| r.get::<_, i64>(0))?
417            .collect::<Result<Vec<_>, _>>()?;
418        Ok(ids)
419    }
420}
421
422/// Deletes entities and their associated vectors. Returns the number of entities removed.
423pub fn delete_entities_by_ids(conn: &Connection, entity_ids: &[i64]) -> Result<usize, AppError> {
424    if entity_ids.is_empty() {
425        return Ok(0);
426    }
427    let mut removed = 0usize;
428    for id in entity_ids {
429        // vec0 lacks FK CASCADE — clean vec_entities explicitly.
430        let _ = conn.execute("DELETE FROM vec_entities WHERE entity_id = ?1", params![id]);
431        let affected = conn.execute("DELETE FROM entities WHERE id = ?1", params![id])?;
432        removed += affected;
433    }
434    Ok(removed)
435}
436
437/// Counts relationships matching the given relation type within a namespace.
438///
439/// Used by `prune-relations --dry-run` to preview the number of relationships
440/// that would be deleted without actually modifying the database.
441///
442/// # Errors
443///
444/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
445pub fn count_relationships_by_relation(
446    conn: &Connection,
447    namespace: &str,
448    relation: &str,
449) -> Result<usize, AppError> {
450    let count: i64 = conn.query_row(
451        "SELECT COUNT(*) FROM relationships WHERE namespace = ?1 AND relation = ?2",
452        params![namespace, relation],
453        |r| r.get(0),
454    )?;
455    Ok(count as usize)
456}
457
458/// Returns unique entity names involved in relationships of the given type.
459///
460/// Queries both source and target sides of every matching relationship row,
461/// deduplicates via `DISTINCT`, and returns the names in alphabetical order.
462///
463/// # Errors
464///
465/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
466pub fn list_entity_names_by_relation(
467    conn: &Connection,
468    namespace: &str,
469    relation: &str,
470) -> Result<Vec<String>, AppError> {
471    let mut stmt = conn.prepare(
472        "SELECT DISTINCT e.name FROM entities e
473         INNER JOIN relationships r ON (e.id = r.source_id OR e.id = r.target_id)
474         WHERE r.namespace = ?1 AND r.relation = ?2
475         ORDER BY e.name",
476    )?;
477    let names: Vec<String> = stmt
478        .query_map(params![namespace, relation], |row| row.get(0))?
479        .collect::<Result<Vec<_>, _>>()?;
480    Ok(names)
481}
482
483/// Deletes all relationships matching a relation type within a namespace.
484///
485/// Operates in chunks of 1000 to avoid holding long write locks and blocking
486/// WAL readers. After deletion, recalculates degree for every affected entity.
487///
488/// Returns `(count_deleted, affected_entity_ids)`.
489///
490/// # Errors
491///
492/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
493pub fn delete_relationships_by_relation(
494    conn: &Connection,
495    namespace: &str,
496    relation: &str,
497) -> Result<(usize, Vec<i64>), AppError> {
498    // Step 1: collect all affected entity IDs before deletion.
499    let mut stmt = conn.prepare(
500        "SELECT DISTINCT source_id FROM relationships WHERE namespace = ?1 AND relation = ?2
501         UNION
502         SELECT DISTINCT target_id FROM relationships WHERE namespace = ?1 AND relation = ?2",
503    )?;
504    let entity_ids: Vec<i64> = stmt
505        .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
506        .collect::<Result<Vec<_>, _>>()?;
507
508    // Step 2: collect relationship IDs to delete.
509    let mut id_stmt =
510        conn.prepare("SELECT id FROM relationships WHERE namespace = ?1 AND relation = ?2")?;
511    let rel_ids: Vec<i64> = id_stmt
512        .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
513        .collect::<Result<Vec<_>, _>>()?;
514
515    // Step 3: delete in chunks of 1000 (memory_relationships + relationships).
516    let mut total_deleted: usize = 0;
517    for chunk in rel_ids.chunks(1000) {
518        for &rel_id in chunk {
519            conn.execute(
520                "DELETE FROM memory_relationships WHERE relationship_id = ?1",
521                params![rel_id],
522            )?;
523            let affected =
524                conn.execute("DELETE FROM relationships WHERE id = ?1", params![rel_id])?;
525            total_deleted += affected;
526        }
527    }
528
529    // Step 4: recalculate degree for all affected entities.
530    for &eid in &entity_ids {
531        recalculate_degree(conn, eid)?;
532    }
533
534    Ok((total_deleted, entity_ids))
535}
536
537pub fn knn_search(
538    conn: &Connection,
539    embedding: &[f32],
540    namespace: &str,
541    k: usize,
542) -> Result<Vec<(i64, f32)>, AppError> {
543    let bytes = f32_to_bytes(embedding);
544    let mut stmt = conn.prepare(
545        "SELECT entity_id, distance FROM vec_entities
546         WHERE embedding MATCH ?1 AND namespace = ?2
547         ORDER BY distance LIMIT ?3",
548    )?;
549    let rows = stmt
550        .query_map(params![bytes, namespace, k as i64], |r| {
551            Ok((r.get::<_, i64>(0)?, r.get::<_, f32>(1)?))
552        })?
553        .collect::<Result<Vec<_>, _>>()?;
554    Ok(rows)
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use crate::constants::EMBEDDING_DIM;
561    use crate::entity_type::EntityType;
562    use crate::storage::connection::register_vec_extension;
563    use rusqlite::Connection;
564    use tempfile::TempDir;
565
566    type TestResult = Result<(), Box<dyn std::error::Error>>;
567
568    fn setup_db() -> Result<(TempDir, Connection), Box<dyn std::error::Error>> {
569        register_vec_extension();
570        let tmp = TempDir::new()?;
571        let db_path = tmp.path().join("test.db");
572        let mut conn = Connection::open(&db_path)?;
573        crate::migrations::runner().run(&mut conn)?;
574        Ok((tmp, conn))
575    }
576
577    fn insert_memory(conn: &Connection) -> Result<i64, Box<dyn std::error::Error>> {
578        conn.execute(
579            "INSERT INTO memories (namespace, name, type, description, body, body_hash)
580             VALUES ('global', 'test-mem', 'user', 'desc', 'body', 'hash1')",
581            [],
582        )?;
583        Ok(conn.last_insert_rowid())
584    }
585
586    fn new_entity_helper(name: &str) -> NewEntity {
587        NewEntity {
588            name: name.to_string(),
589            entity_type: EntityType::Project,
590            description: None,
591        }
592    }
593
594    fn embedding_zero() -> Vec<f32> {
595        vec![0.0f32; EMBEDDING_DIM]
596    }
597
598    // ------------------------------------------------------------------ //
599    // upsert_entity
600    // ------------------------------------------------------------------ //
601
602    #[test]
603    fn test_upsert_entity_creates_new() -> TestResult {
604        let (_tmp, conn) = setup_db()?;
605        let e = new_entity_helper("projeto-alpha");
606        let id = upsert_entity(&conn, "global", &e)?;
607        assert!(id > 0);
608        Ok(())
609    }
610
611    #[test]
612    fn test_upsert_entity_idempotent_returns_same_id() -> TestResult {
613        let (_tmp, conn) = setup_db()?;
614        let e = new_entity_helper("projeto-beta");
615        let id1 = upsert_entity(&conn, "global", &e)?;
616        let id2 = upsert_entity(&conn, "global", &e)?;
617        assert_eq!(id1, id2);
618        Ok(())
619    }
620
621    #[test]
622    fn test_upsert_entity_updates_description() -> TestResult {
623        let (_tmp, conn) = setup_db()?;
624        let e1 = new_entity_helper("projeto-gamma");
625        let id1 = upsert_entity(&conn, "global", &e1)?;
626
627        let e2 = NewEntity {
628            name: "projeto-gamma".to_string(),
629            entity_type: EntityType::Tool,
630            description: Some("nova desc".to_string()),
631        };
632        let id2 = upsert_entity(&conn, "global", &e2)?;
633        assert_eq!(id1, id2);
634
635        let desc: Option<String> = conn.query_row(
636            "SELECT description FROM entities WHERE id = ?1",
637            params![id1],
638            |r| r.get(0),
639        )?;
640        assert_eq!(desc.as_deref(), Some("nova desc"));
641        Ok(())
642    }
643
644    #[test]
645    fn test_upsert_entity_different_namespaces_create_distinct_records() -> TestResult {
646        let (_tmp, conn) = setup_db()?;
647        let e = new_entity_helper("compartilhada");
648        let id1 = upsert_entity(&conn, "ns1", &e)?;
649        let id2 = upsert_entity(&conn, "ns2", &e)?;
650        assert_ne!(id1, id2);
651        Ok(())
652    }
653
654    // ------------------------------------------------------------------ //
655    // upsert_entity_vec — covers DELETE+INSERT (new branch after the OOM fix)
656    // ------------------------------------------------------------------ //
657
658    #[test]
659    fn test_upsert_entity_vec_first_time_without_conflict() -> TestResult {
660        let (_tmp, conn) = setup_db()?;
661        let e = new_entity_helper("vec-nova");
662        let entity_id = upsert_entity(&conn, "global", &e)?;
663        let emb = embedding_zero();
664
665        let result = upsert_entity_vec(
666            &conn,
667            entity_id,
668            "global",
669            EntityType::Project,
670            &emb,
671            "vec-nova",
672        );
673        assert!(result.is_ok(), "first insertion must succeed");
674
675        let count: i64 = conn.query_row(
676            "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
677            params![entity_id],
678            |r| r.get(0),
679        )?;
680        assert_eq!(count, 1, "must have exactly one row after insertion");
681        Ok(())
682    }
683
684    #[test]
685    fn test_upsert_entity_vec_second_time_replaces_without_error() -> TestResult {
686        // Covers the branch where DELETE removes the existing row before INSERT.
687        let (_tmp, conn) = setup_db()?;
688        let e = new_entity_helper("vec-existente");
689        let entity_id = upsert_entity(&conn, "global", &e)?;
690        let emb = embedding_zero();
691
692        upsert_entity_vec(
693            &conn,
694            entity_id,
695            "global",
696            EntityType::Project,
697            &emb,
698            "vec-existente",
699        )?;
700
701        // Second call: DELETE returns 1 removed row, INSERT must succeed.
702        let result = upsert_entity_vec(
703            &conn,
704            entity_id,
705            "global",
706            EntityType::Tool,
707            &emb,
708            "vec-existente",
709        );
710        assert!(
711            result.is_ok(),
712            "second insertion (replace) must succeed: {result:?}"
713        );
714
715        let count: i64 = conn.query_row(
716            "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
717            params![entity_id],
718            |r| r.get(0),
719        )?;
720        assert_eq!(count, 1, "must have exactly one row after replacement");
721        Ok(())
722    }
723
724    #[test]
725    fn test_upsert_entity_vec_multiple_independent_entities() -> TestResult {
726        let (_tmp, conn) = setup_db()?;
727        let emb = embedding_zero();
728
729        for i in 0..3i64 {
730            let nome = format!("ent-{i}");
731            let e = new_entity_helper(&nome);
732            let entity_id = upsert_entity(&conn, "global", &e)?;
733            upsert_entity_vec(&conn, entity_id, "global", EntityType::Project, &emb, &nome)?;
734        }
735
736        let count: i64 = conn.query_row("SELECT COUNT(*) FROM vec_entities", [], |r| r.get(0))?;
737        assert_eq!(count, 3, "must have three distinct rows in vec_entities");
738        Ok(())
739    }
740
741    // ------------------------------------------------------------------ //
742    // find_entity_id
743    // ------------------------------------------------------------------ //
744
745    #[test]
746    fn test_find_entity_id_existing_returns_some() -> TestResult {
747        let (_tmp, conn) = setup_db()?;
748        let e = new_entity_helper("entidade-busca");
749        let id_inserido = upsert_entity(&conn, "global", &e)?;
750        let id_encontrado = find_entity_id(&conn, "global", "entidade-busca")?;
751        assert_eq!(id_encontrado, Some(id_inserido));
752        Ok(())
753    }
754
755    #[test]
756    fn test_find_entity_id_missing_returns_none() -> TestResult {
757        let (_tmp, conn) = setup_db()?;
758        let id = find_entity_id(&conn, "global", "nao-existe")?;
759        assert_eq!(id, None);
760        Ok(())
761    }
762
763    // ------------------------------------------------------------------ //
764    // delete_entities_by_ids
765    // ------------------------------------------------------------------ //
766
767    #[test]
768    fn test_delete_entities_by_ids_empty_list_returns_zero() -> TestResult {
769        let (_tmp, conn) = setup_db()?;
770        let removed = delete_entities_by_ids(&conn, &[])?;
771        assert_eq!(removed, 0);
772        Ok(())
773    }
774
775    #[test]
776    fn test_delete_entities_by_ids_removes_valid_entity() -> TestResult {
777        let (_tmp, conn) = setup_db()?;
778        let e = new_entity_helper("to-delete");
779        let entity_id = upsert_entity(&conn, "global", &e)?;
780
781        let removed = delete_entities_by_ids(&conn, &[entity_id])?;
782        assert_eq!(removed, 1);
783
784        let id = find_entity_id(&conn, "global", "to-delete")?;
785        assert_eq!(id, None, "entity must have been removed");
786        Ok(())
787    }
788
789    #[test]
790    fn test_delete_entities_by_ids_missing_id_returns_zero() -> TestResult {
791        let (_tmp, conn) = setup_db()?;
792        let removed = delete_entities_by_ids(&conn, &[9999])?;
793        assert_eq!(removed, 0);
794        Ok(())
795    }
796
797    #[test]
798    fn test_delete_entities_by_ids_removes_multiple() -> TestResult {
799        let (_tmp, conn) = setup_db()?;
800        let id1 = upsert_entity(&conn, "global", &new_entity_helper("del-a"))?;
801        let id2 = upsert_entity(&conn, "global", &new_entity_helper("del-b"))?;
802        let id3 = upsert_entity(&conn, "global", &new_entity_helper("del-c"))?;
803
804        let removed = delete_entities_by_ids(&conn, &[id1, id2])?;
805        assert_eq!(removed, 2);
806
807        assert!(find_entity_id(&conn, "global", "del-a")?.is_none());
808        assert!(find_entity_id(&conn, "global", "del-b")?.is_none());
809        assert!(find_entity_id(&conn, "global", "del-c")?.is_some());
810        let _ = id3;
811        Ok(())
812    }
813
814    #[test]
815    fn test_delete_entities_by_ids_also_removes_vec() -> TestResult {
816        let (_tmp, conn) = setup_db()?;
817        let e = new_entity_helper("del-com-vec");
818        let entity_id = upsert_entity(&conn, "global", &e)?;
819        let emb = embedding_zero();
820        upsert_entity_vec(
821            &conn,
822            entity_id,
823            "global",
824            EntityType::Project,
825            &emb,
826            "del-com-vec",
827        )?;
828
829        let count_antes: i64 = conn.query_row(
830            "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
831            params![entity_id],
832            |r| r.get(0),
833        )?;
834        assert_eq!(count_antes, 1);
835
836        delete_entities_by_ids(&conn, &[entity_id])?;
837
838        let count_depois: i64 = conn.query_row(
839            "SELECT COUNT(*) FROM vec_entities WHERE entity_id = ?1",
840            params![entity_id],
841            |r| r.get(0),
842        )?;
843        assert_eq!(
844            count_depois, 0,
845            "vec_entities deve ser limpo junto com entities"
846        );
847        Ok(())
848    }
849
850    // ------------------------------------------------------------------ //
851    // upsert_relationship / find_relationship
852    // ------------------------------------------------------------------ //
853
854    #[test]
855    fn test_upsert_relationship_creates_new() -> TestResult {
856        let (_tmp, conn) = setup_db()?;
857        let id_a = upsert_entity(&conn, "global", &new_entity_helper("rel-a"))?;
858        let id_b = upsert_entity(&conn, "global", &new_entity_helper("rel-b"))?;
859
860        let rel = NewRelationship {
861            source: "rel-a".to_string(),
862            target: "rel-b".to_string(),
863            relation: "uses".to_string(),
864            strength: 0.8,
865            description: None,
866        };
867        let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
868        assert!(rel_id > 0);
869        Ok(())
870    }
871
872    #[test]
873    fn test_upsert_relationship_idempotent() -> TestResult {
874        let (_tmp, conn) = setup_db()?;
875        let id_a = upsert_entity(&conn, "global", &new_entity_helper("idem-a"))?;
876        let id_b = upsert_entity(&conn, "global", &new_entity_helper("idem-b"))?;
877
878        let rel = NewRelationship {
879            source: "idem-a".to_string(),
880            target: "idem-b".to_string(),
881            relation: "uses".to_string(),
882            strength: 0.5,
883            description: None,
884        };
885        let id1 = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
886        let id2 = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
887        assert_eq!(id1, id2);
888        Ok(())
889    }
890
891    #[test]
892    fn test_find_relationship_existing() -> TestResult {
893        let (_tmp, conn) = setup_db()?;
894        let id_a = upsert_entity(&conn, "global", &new_entity_helper("fr-a"))?;
895        let id_b = upsert_entity(&conn, "global", &new_entity_helper("fr-b"))?;
896
897        let rel = NewRelationship {
898            source: "fr-a".to_string(),
899            target: "fr-b".to_string(),
900            relation: "depends_on".to_string(),
901            strength: 0.7,
902            description: None,
903        };
904        upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
905
906        let encontrada = find_relationship(&conn, id_a, id_b, "depends_on")?;
907        let row = encontrada.ok_or("relationship should exist")?;
908        assert_eq!(row.source_id, id_a);
909        assert_eq!(row.target_id, id_b);
910        assert!((row.weight - 0.7).abs() < 1e-9);
911        Ok(())
912    }
913
914    #[test]
915    fn test_find_relationship_missing_returns_none() -> TestResult {
916        let (_tmp, conn) = setup_db()?;
917        let resultado = find_relationship(&conn, 9999, 8888, "uses")?;
918        assert!(resultado.is_none());
919        Ok(())
920    }
921
922    // ------------------------------------------------------------------ //
923    // link_memory_entity / link_memory_relationship
924    // ------------------------------------------------------------------ //
925
926    #[test]
927    fn test_link_memory_entity_idempotent() -> TestResult {
928        let (_tmp, conn) = setup_db()?;
929        let memory_id = insert_memory(&conn)?;
930        let entity_id = upsert_entity(&conn, "global", &new_entity_helper("me-ent"))?;
931
932        link_memory_entity(&conn, memory_id, entity_id)?;
933        let resultado = link_memory_entity(&conn, memory_id, entity_id);
934        assert!(
935            resultado.is_ok(),
936            "INSERT OR IGNORE must not fail on duplicate"
937        );
938        Ok(())
939    }
940
941    #[test]
942    fn test_link_memory_relationship_idempotent() -> TestResult {
943        let (_tmp, conn) = setup_db()?;
944        let memory_id = insert_memory(&conn)?;
945        let id_a = upsert_entity(&conn, "global", &new_entity_helper("mr-a"))?;
946        let id_b = upsert_entity(&conn, "global", &new_entity_helper("mr-b"))?;
947
948        let rel = NewRelationship {
949            source: "mr-a".to_string(),
950            target: "mr-b".to_string(),
951            relation: "uses".to_string(),
952            strength: 0.5,
953            description: None,
954        };
955        let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
956
957        link_memory_relationship(&conn, memory_id, rel_id)?;
958        let resultado = link_memory_relationship(&conn, memory_id, rel_id);
959        assert!(
960            resultado.is_ok(),
961            "INSERT OR IGNORE must not fail on duplicate"
962        );
963        Ok(())
964    }
965
966    // ------------------------------------------------------------------ //
967    // increment_degree / recalculate_degree
968    // ------------------------------------------------------------------ //
969
970    #[test]
971    fn test_increment_degree_increases_counter() -> TestResult {
972        let (_tmp, conn) = setup_db()?;
973        let entity_id = upsert_entity(&conn, "global", &new_entity_helper("grau-ent"))?;
974
975        increment_degree(&conn, entity_id)?;
976        increment_degree(&conn, entity_id)?;
977
978        let degree: i64 = conn.query_row(
979            "SELECT degree FROM entities WHERE id = ?1",
980            params![entity_id],
981            |r| r.get(0),
982        )?;
983        assert_eq!(degree, 2);
984        Ok(())
985    }
986
987    #[test]
988    fn test_recalculate_degree_reflects_actual_relations() -> TestResult {
989        let (_tmp, conn) = setup_db()?;
990        let id_a = upsert_entity(&conn, "global", &new_entity_helper("rc-a"))?;
991        let id_b = upsert_entity(&conn, "global", &new_entity_helper("rc-b"))?;
992        let id_c = upsert_entity(&conn, "global", &new_entity_helper("rc-c"))?;
993
994        let rel1 = NewRelationship {
995            source: "rc-a".to_string(),
996            target: "rc-b".to_string(),
997            relation: "uses".to_string(),
998            strength: 0.5,
999            description: None,
1000        };
1001        let rel2 = NewRelationship {
1002            source: "rc-c".to_string(),
1003            target: "rc-a".to_string(),
1004            relation: "depends_on".to_string(),
1005            strength: 0.5,
1006            description: None,
1007        };
1008        upsert_relationship(&conn, "global", id_a, id_b, &rel1)?;
1009        upsert_relationship(&conn, "global", id_c, id_a, &rel2)?;
1010
1011        recalculate_degree(&conn, id_a)?;
1012
1013        let degree: i64 = conn.query_row(
1014            "SELECT degree FROM entities WHERE id = ?1",
1015            params![id_a],
1016            |r| r.get(0),
1017        )?;
1018        assert_eq!(
1019            degree, 2,
1020            "rc-a appears in two relationships (source+target)"
1021        );
1022        Ok(())
1023    }
1024
1025    // ------------------------------------------------------------------ //
1026    // find_orphan_entity_ids
1027    // ------------------------------------------------------------------ //
1028
1029    #[test]
1030    fn test_find_orphan_entity_ids_without_orphans() -> TestResult {
1031        let (_tmp, conn) = setup_db()?;
1032        let memory_id = insert_memory(&conn)?;
1033        let entity_id = upsert_entity(&conn, "global", &new_entity_helper("nao-orfa"))?;
1034        link_memory_entity(&conn, memory_id, entity_id)?;
1035
1036        let orfas = find_orphan_entity_ids(&conn, Some("global"))?;
1037        assert!(!orfas.contains(&entity_id));
1038        Ok(())
1039    }
1040
1041    #[test]
1042    fn test_find_orphan_entity_ids_detects_orphans() -> TestResult {
1043        let (_tmp, conn) = setup_db()?;
1044        let entity_id = upsert_entity(&conn, "global", &new_entity_helper("sim-orfa"))?;
1045
1046        let orfas = find_orphan_entity_ids(&conn, Some("global"))?;
1047        assert!(orfas.contains(&entity_id));
1048        Ok(())
1049    }
1050
1051    #[test]
1052    fn test_find_orphan_entity_ids_without_namespace_returns_all() -> TestResult {
1053        let (_tmp, conn) = setup_db()?;
1054        let id1 = upsert_entity(&conn, "ns-a", &new_entity_helper("orfa-a"))?;
1055        let id2 = upsert_entity(&conn, "ns-b", &new_entity_helper("orfa-b"))?;
1056
1057        let orfas = find_orphan_entity_ids(&conn, None)?;
1058        assert!(orfas.contains(&id1));
1059        assert!(orfas.contains(&id2));
1060        Ok(())
1061    }
1062
1063    // ------------------------------------------------------------------ //
1064    // list_entities / list_relationships_by_namespace
1065    // ------------------------------------------------------------------ //
1066
1067    #[test]
1068    fn test_list_entities_with_namespace() -> TestResult {
1069        let (_tmp, conn) = setup_db()?;
1070        upsert_entity(&conn, "le-ns", &new_entity_helper("le-ent-1"))?;
1071        upsert_entity(&conn, "le-ns", &new_entity_helper("le-ent-2"))?;
1072        upsert_entity(&conn, "outro-ns", &new_entity_helper("le-ent-3"))?;
1073
1074        let lista = list_entities(&conn, Some("le-ns"))?;
1075        assert_eq!(lista.len(), 2);
1076        assert!(lista.iter().all(|e| e.namespace == "le-ns"));
1077        Ok(())
1078    }
1079
1080    #[test]
1081    fn test_list_entities_without_namespace_returns_all() -> TestResult {
1082        let (_tmp, conn) = setup_db()?;
1083        upsert_entity(&conn, "ns1", &new_entity_helper("all-ent-1"))?;
1084        upsert_entity(&conn, "ns2", &new_entity_helper("all-ent-2"))?;
1085
1086        let lista = list_entities(&conn, None)?;
1087        assert!(lista.len() >= 2);
1088        Ok(())
1089    }
1090
1091    #[test]
1092    fn test_list_relationships_by_namespace_filters_correctly() -> TestResult {
1093        let (_tmp, conn) = setup_db()?;
1094        let id_a = upsert_entity(&conn, "rel-ns", &new_entity_helper("lr-a"))?;
1095        let id_b = upsert_entity(&conn, "rel-ns", &new_entity_helper("lr-b"))?;
1096
1097        let rel = NewRelationship {
1098            source: "lr-a".to_string(),
1099            target: "lr-b".to_string(),
1100            relation: "uses".to_string(),
1101            strength: 0.5,
1102            description: None,
1103        };
1104        upsert_relationship(&conn, "rel-ns", id_a, id_b, &rel)?;
1105
1106        let lista = list_relationships_by_namespace(&conn, Some("rel-ns"))?;
1107        assert!(!lista.is_empty());
1108        assert!(lista.iter().all(|r| r.namespace == "rel-ns"));
1109        Ok(())
1110    }
1111
1112    // ------------------------------------------------------------------ //
1113    // delete_relationship_by_id / create_or_fetch_relationship
1114    // ------------------------------------------------------------------ //
1115
1116    #[test]
1117    fn test_delete_relationship_by_id_removes_relation() -> TestResult {
1118        let (_tmp, conn) = setup_db()?;
1119        let id_a = upsert_entity(&conn, "global", &new_entity_helper("dr-a"))?;
1120        let id_b = upsert_entity(&conn, "global", &new_entity_helper("dr-b"))?;
1121
1122        let rel = NewRelationship {
1123            source: "dr-a".to_string(),
1124            target: "dr-b".to_string(),
1125            relation: "uses".to_string(),
1126            strength: 0.5,
1127            description: None,
1128        };
1129        let rel_id = upsert_relationship(&conn, "global", id_a, id_b, &rel)?;
1130
1131        delete_relationship_by_id(&conn, rel_id)?;
1132
1133        let encontrada = find_relationship(&conn, id_a, id_b, "uses")?;
1134        assert!(encontrada.is_none(), "relationship must have been removed");
1135        Ok(())
1136    }
1137
1138    #[test]
1139    fn test_create_or_fetch_relationship_creates_new() -> TestResult {
1140        let (_tmp, conn) = setup_db()?;
1141        let id_a = upsert_entity(&conn, "global", &new_entity_helper("cf-a"))?;
1142        let id_b = upsert_entity(&conn, "global", &new_entity_helper("cf-b"))?;
1143
1144        let (rel_id, created) =
1145            create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1146        assert!(rel_id > 0);
1147        assert!(created);
1148        Ok(())
1149    }
1150
1151    #[test]
1152    fn test_create_or_fetch_relationship_returns_existing() -> TestResult {
1153        let (_tmp, conn) = setup_db()?;
1154        let id_a = upsert_entity(&conn, "global", &new_entity_helper("cf2-a"))?;
1155        let id_b = upsert_entity(&conn, "global", &new_entity_helper("cf2-b"))?;
1156
1157        create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1158        let (_, created) =
1159            create_or_fetch_relationship(&conn, "global", id_a, id_b, "uses", 0.5, None)?;
1160        assert!(
1161            !created,
1162            "second call must return the existing relationship"
1163        );
1164        Ok(())
1165    }
1166
1167    // ------------------------------------------------------------------ //
1168    // serde alias: field "type" accepted as a synonym for "entity_type"
1169    // ------------------------------------------------------------------ //
1170
1171    #[test]
1172    fn accepts_type_field_as_alias() -> TestResult {
1173        let json = r#"{"name": "X", "type": "concept"}"#;
1174        let ent: NewEntity = serde_json::from_str(json)?;
1175        assert_eq!(ent.entity_type, EntityType::Concept);
1176        Ok(())
1177    }
1178
1179    #[test]
1180    fn accepts_canonical_entity_type_field() -> TestResult {
1181        let json = r#"{"name": "X", "entity_type": "concept"}"#;
1182        let ent: NewEntity = serde_json::from_str(json)?;
1183        assert_eq!(ent.entity_type, EntityType::Concept);
1184        Ok(())
1185    }
1186
1187    #[test]
1188    fn both_fields_present_yields_duplicate_error() {
1189        // having both entity_type and type in the same JSON is a duplicate and must fail
1190        let json = r#"{"name": "X", "entity_type": "concept", "type": "person"}"#;
1191        let resultado: Result<NewEntity, _> = serde_json::from_str(json);
1192        assert!(
1193            resultado.is_err(),
1194            "both fields in the same JSON are a duplicate"
1195        );
1196    }
1197}