Skip to main content

sqlite_graphrag/storage/entities/
merge.rs

1//! Graph-binding and merge-support helpers for entities/relationships.
2//!
3//! Covers junction-table links (`memory_entities` / `memory_relationships`),
4//! degree maintenance, relationship create/fetch/delete, and orphan cleanup —
5//! the storage primitives used by `merge-entities`, force-merge remember/ingest
6//! paths, and `prune-relations` / `cleanup-orphans`.
7
8use crate::errors::AppError;
9use crate::parsers::normalize_entity_name;
10use rusqlite::{params, Connection};
11use serde::Serialize;
12/// Links a memory to an entity in the `memory_entities` join table.
13///
14/// # Errors
15///
16/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
17pub fn link_memory_entity(
18    conn: &Connection,
19    memory_id: i64,
20    entity_id: i64,
21) -> Result<(), AppError> {
22    conn.execute(
23        "INSERT OR IGNORE INTO memory_entities (memory_id, entity_id) VALUES (?1, ?2)",
24        params![memory_id, entity_id],
25    )?;
26    Ok(())
27}
28
29/// Links a memory to a relationship in the `memory_relationships` join table.
30///
31/// # Errors
32///
33/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
34pub fn link_memory_relationship(
35    conn: &Connection,
36    memory_id: i64,
37    rel_id: i64,
38) -> Result<(), AppError> {
39    conn.execute(
40        "INSERT OR IGNORE INTO memory_relationships (memory_id, relationship_id) VALUES (?1, ?2)",
41        params![memory_id, rel_id],
42    )?;
43    Ok(())
44}
45
46/// GAP-SG-52: removes the curated `memory_entities` binding between a memory
47/// and an entity. Unlike `prune-ner` (which targets an entity across every
48/// memory), this surgically unlinks a single `(memory_id, entity_id)` pair —
49/// covering bindings created via `remember --graph-stdin`. Returns the number
50/// of junction rows removed (0 or 1).
51///
52/// # Errors
53///
54/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
55pub fn unlink_memory_entity(
56    conn: &Connection,
57    memory_id: i64,
58    entity_id: i64,
59) -> Result<u64, AppError> {
60    let affected = conn.execute(
61        "DELETE FROM memory_entities WHERE memory_id = ?1 AND entity_id = ?2",
62        params![memory_id, entity_id],
63    )?;
64    Ok(affected as u64)
65}
66
67/// GAP-SG-51: clears every `memory_entities` and `memory_relationships`
68/// binding for a memory so a `--force-merge --replace-graph` update can install
69/// an authoritative set (including the empty set). The entities and
70/// relationships themselves are preserved; only the junction rows for this
71/// memory are removed. Returns `(entity_bindings_removed, relationship_bindings_removed)`.
72///
73/// # Errors
74///
75/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
76pub fn clear_memory_graph_bindings(
77    conn: &Connection,
78    memory_id: i64,
79) -> Result<(u64, u64), AppError> {
80    let entities_removed = conn.execute(
81        "DELETE FROM memory_entities WHERE memory_id = ?1",
82        params![memory_id],
83    )? as u64;
84    let rels_removed = conn.execute(
85        "DELETE FROM memory_relationships WHERE memory_id = ?1",
86        params![memory_id],
87    )? as u64;
88    Ok((entities_removed, rels_removed))
89}
90
91/// Increments the `degree` counter of an entity by one.
92///
93/// # Errors
94///
95/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
96pub fn increment_degree(conn: &Connection, entity_id: i64) -> Result<(), AppError> {
97    conn.execute(
98        "UPDATE entities SET degree = degree + 1 WHERE id = ?1",
99        params![entity_id],
100    )?;
101    Ok(())
102}
103
104/// Looks up the entity by name and namespace. Returns the id when it exists.
105///
106/// # Errors
107///
108/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
109pub fn find_entity_id(
110    conn: &Connection,
111    namespace: &str,
112    name: &str,
113) -> Result<Option<i64>, AppError> {
114    // Normalize the lookup name so it matches the normalized names written by
115    // `upsert_entity`. Without this, an entity written through normalization
116    // (e.g. "Foo Bar" -> "foo-bar") would be unreachable by its original
117    // spelling, breaking delete-entity, reclassify, merge-entities, rename and
118    // memory-entities lookups.
119    let name = normalize_entity_name(name);
120    let mut stmt =
121        conn.prepare_cached("SELECT id FROM entities WHERE namespace = ?1 AND name = ?2")?;
122    match stmt.query_row(params![namespace, &name], |r| r.get::<_, i64>(0)) {
123        Ok(id) => Ok(Some(id)),
124        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
125        Err(e) => Err(AppError::Database(e)),
126    }
127}
128
129/// Structure representing an existing relation.
130#[derive(Debug, Serialize)]
131pub struct RelationshipRow {
132    /// Unique identifier.
133    pub id: i64,
134    /// Namespace scope.
135    pub namespace: String,
136    /// Source ID.
137    pub source_id: i64,
138    /// Target ID.
139    pub target_id: i64,
140    /// Relationship type.
141    pub relation: String,
142    /// Relationship weight.
143    pub weight: f64,
144    /// Human-readable description.
145    pub description: Option<String>,
146}
147
148/// Looks up a specific relation by (source_id, target_id, relation).
149///
150/// # Errors
151///
152/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
153pub fn find_relationship(
154    conn: &Connection,
155    source_id: i64,
156    target_id: i64,
157    relation: &str,
158) -> Result<Option<RelationshipRow>, AppError> {
159    let mut stmt = conn.prepare_cached(
160        "SELECT id, namespace, source_id, target_id, relation, weight, description
161         FROM relationships
162         WHERE source_id = ?1 AND target_id = ?2 AND relation = ?3",
163    )?;
164    match stmt.query_row(params![source_id, target_id, relation], |r| {
165        Ok(RelationshipRow {
166            id: r.get(0)?,
167            namespace: r.get(1)?,
168            source_id: r.get(2)?,
169            target_id: r.get(3)?,
170            relation: r.get(4)?,
171            weight: r.get(5)?,
172            description: r.get(6)?,
173        })
174    }) {
175        Ok(row) => Ok(Some(row)),
176        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
177        Err(e) => Err(AppError::Database(e)),
178    }
179}
180
181/// Creates a relation if it does not exist (returns action="created")
182/// or returns the existing relation (action="already_exists") with updated weight.
183///
184/// # Errors
185///
186/// - [`AppError::Database`] — SQLite query or constraint failure.
187/// - [`AppError::Validation`] — self-link attempt (source equals target).
188pub fn create_or_fetch_relationship(
189    conn: &Connection,
190    namespace: &str,
191    source_id: i64,
192    target_id: i64,
193    relation: &str,
194    weight: f64,
195    description: Option<&str>,
196) -> Result<(i64, bool), AppError> {
197    // Check if it exists first; update weight if different.
198    let existing = find_relationship(conn, source_id, target_id, relation)?;
199    if let Some(row) = existing {
200        if (row.weight - weight).abs() > f64::EPSILON {
201            conn.execute(
202                "UPDATE relationships SET weight = ?1 WHERE id = ?2",
203                params![weight, row.id],
204            )?;
205        }
206        return Ok((row.id, false));
207    }
208    conn.execute(
209        "INSERT INTO relationships (namespace, source_id, target_id, relation, weight, description)
210         VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
211        params![
212            namespace,
213            source_id,
214            target_id,
215            relation,
216            weight,
217            description
218        ],
219    )?;
220    let id: i64 = conn.query_row(
221        "SELECT id FROM relationships WHERE source_id = ?1 AND target_id = ?2 AND relation = ?3",
222        params![source_id, target_id, relation],
223        |r| r.get(0),
224    )?;
225    Ok((id, true))
226}
227
228/// Removes a relation by id and cleans up memory_relationships.
229///
230/// # Errors
231///
232/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
233pub fn delete_relationship_by_id(conn: &Connection, relationship_id: i64) -> Result<(), AppError> {
234    conn.execute(
235        "DELETE FROM memory_relationships WHERE relationship_id = ?1",
236        params![relationship_id],
237    )?;
238    conn.execute(
239        "DELETE FROM relationships WHERE id = ?1",
240        params![relationship_id],
241    )?;
242    Ok(())
243}
244
245/// Recalculates the `degree` field of an entity.
246///
247/// # Errors
248///
249/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
250pub fn recalculate_degree(conn: &Connection, entity_id: i64) -> Result<(), AppError> {
251    conn.execute(
252        "UPDATE entities
253         SET degree = (SELECT COUNT(*) FROM relationships
254                       WHERE source_id = entities.id OR target_id = entities.id)
255         WHERE id = ?1",
256        params![entity_id],
257    )?;
258    Ok(())
259}
260
261/// Locates orphan entities: no link in memory_entities and no relations.
262///
263/// # Errors
264///
265/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
266pub fn find_orphan_entity_ids(
267    conn: &Connection,
268    namespace: Option<&str>,
269) -> Result<Vec<i64>, AppError> {
270    if let Some(ns) = namespace {
271        let mut stmt = conn.prepare_cached(
272            "SELECT e.id FROM entities e
273             WHERE e.namespace = ?1
274               AND NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
275               AND NOT EXISTS (
276                   SELECT 1 FROM relationships r
277                   WHERE r.source_id = e.id OR r.target_id = e.id
278               )",
279        )?;
280        let ids = stmt
281            .query_map(params![ns], |r| r.get::<_, i64>(0))?
282            .collect::<Result<Vec<_>, _>>()?;
283        Ok(ids)
284    } else {
285        let mut stmt = conn.prepare_cached(
286            "SELECT e.id FROM entities e
287             WHERE NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
288               AND NOT EXISTS (
289                   SELECT 1 FROM relationships r
290                   WHERE r.source_id = e.id OR r.target_id = e.id
291               )",
292        )?;
293        let ids = stmt
294            .query_map([], |r| r.get::<_, i64>(0))?
295            .collect::<Result<Vec<_>, _>>()?;
296        Ok(ids)
297    }
298}
299
300/// Deletes entities and their associated vectors. Returns the number of entities removed.
301///
302/// # Errors
303///
304/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
305pub fn delete_entities_by_ids(conn: &Connection, entity_ids: &[i64]) -> Result<usize, AppError> {
306    if entity_ids.is_empty() {
307        return Ok(0);
308    }
309    let mut removed = 0usize;
310    for id in entity_ids {
311        // FK CASCADE on entity_embeddings handles cleanup automatically.
312        let _ = conn.execute("DELETE FROM vec_entities WHERE entity_id = ?1", params![id]);
313        let affected = conn.execute("DELETE FROM entities WHERE id = ?1", params![id])?;
314        removed += affected;
315    }
316    Ok(removed)
317}
318
319/// Counts relationships matching the given relation type within a namespace.
320///
321/// Used by `prune-relations --dry-run` to preview the number of relationships
322/// that would be deleted without actually modifying the database.
323///
324/// # Errors
325///
326/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
327pub fn count_relationships_by_relation(
328    conn: &Connection,
329    namespace: &str,
330    relation: &str,
331) -> Result<usize, AppError> {
332    let count: i64 = conn.query_row(
333        "SELECT COUNT(*) FROM relationships WHERE namespace = ?1 AND relation = ?2",
334        params![namespace, relation],
335        |r| r.get(0),
336    )?;
337    Ok(count as usize)
338}
339
340/// Returns unique entity names involved in relationships of the given type.
341///
342/// Queries both source and target sides of every matching relationship row,
343/// deduplicates via `DISTINCT`, and returns the names in alphabetical order.
344///
345/// # Errors
346///
347/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
348pub fn list_entity_names_by_relation(
349    conn: &Connection,
350    namespace: &str,
351    relation: &str,
352) -> Result<Vec<String>, AppError> {
353    let mut stmt = conn.prepare_cached(
354        "SELECT DISTINCT e.name FROM entities e
355         INNER JOIN relationships r ON (e.id = r.source_id OR e.id = r.target_id)
356         WHERE r.namespace = ?1 AND r.relation = ?2
357         ORDER BY e.name",
358    )?;
359    let names: Vec<String> = stmt
360        .query_map(params![namespace, relation], |row| row.get(0))?
361        .collect::<Result<Vec<_>, _>>()?;
362    Ok(names)
363}
364
365/// Deletes all relationships matching a relation type within a namespace.
366///
367/// Operates in chunks of 1000 to avoid holding long write locks and blocking
368/// WAL readers. After deletion, recalculates degree for every affected entity.
369///
370/// Returns `(count_deleted, affected_entity_ids)`.
371///
372/// # Errors
373///
374/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
375pub fn delete_relationships_by_relation(
376    conn: &Connection,
377    namespace: &str,
378    relation: &str,
379) -> Result<(usize, Vec<i64>), AppError> {
380    // Step 1: collect all affected entity IDs before deletion.
381    let mut stmt = conn.prepare_cached(
382        "SELECT DISTINCT source_id FROM relationships WHERE namespace = ?1 AND relation = ?2
383         UNION
384         SELECT DISTINCT target_id FROM relationships WHERE namespace = ?1 AND relation = ?2",
385    )?;
386    let entity_ids: Vec<i64> = stmt
387        .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
388        .collect::<Result<Vec<_>, _>>()?;
389
390    // Step 2: collect relationship IDs to delete.
391    let mut id_stmt =
392        conn.prepare_cached("SELECT id FROM relationships WHERE namespace = ?1 AND relation = ?2")?;
393    let rel_ids: Vec<i64> = id_stmt
394        .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
395        .collect::<Result<Vec<_>, _>>()?;
396
397    // Step 3: delete in chunks of 1000 (memory_relationships + relationships).
398    let mut total_deleted: usize = 0;
399    for chunk in rel_ids.chunks(1000) {
400        for &rel_id in chunk {
401            conn.execute(
402                "DELETE FROM memory_relationships WHERE relationship_id = ?1",
403                params![rel_id],
404            )?;
405            let affected =
406                conn.execute("DELETE FROM relationships WHERE id = ?1", params![rel_id])?;
407            total_deleted += affected;
408        }
409    }
410
411    // Step 4: recalculate degree for all affected entities.
412    for &eid in &entity_ids {
413        recalculate_degree(conn, eid)?;
414    }
415
416    Ok((total_deleted, entity_ids))
417}