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    // v1.2.8: the label is canonicalised HERE, at the last step before SQL,
198    // rather than in each caller. Four write paths reached this function and
199    // three of them normalised; the fourth — `enrich::extraction_body` — passed
200    // the extraction model's string verbatim, and it is the one that runs in
201    // bulk. That single omission produced 67 651 edges in a spelling every read
202    // filter normalises away, so the rows exist and no query can reach them.
203    //
204    // Fixing that call site alone would leave the next one free to repeat it.
205    // Owning the invariant at the persistence boundary makes the omission
206    // impossible instead of merely absent, and the callers that already
207    // normalise are unaffected because the operation is idempotent.
208    let relation = &crate::parsers::map_to_canonical_relation(relation);
209    // Check if it exists first; update weight if different.
210    let existing = find_relationship(conn, source_id, target_id, relation)?;
211    if let Some(row) = existing {
212        if (row.weight - weight).abs() > f64::EPSILON {
213            conn.execute(
214                "UPDATE relationships SET weight = ?1 WHERE id = ?2",
215                params![weight, row.id],
216            )?;
217        }
218        return Ok((row.id, false));
219    }
220    conn.execute(
221        "INSERT INTO relationships (namespace, source_id, target_id, relation, weight, description)
222         VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
223        params![
224            namespace,
225            source_id,
226            target_id,
227            relation,
228            weight,
229            description
230        ],
231    )?;
232    let id: i64 = conn.query_row(
233        "SELECT id FROM relationships WHERE source_id = ?1 AND target_id = ?2 AND relation = ?3",
234        params![source_id, target_id, relation],
235        |r| r.get(0),
236    )?;
237    Ok((id, true))
238}
239
240/// Removes a relation by id and cleans up memory_relationships.
241///
242/// # Errors
243///
244/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
245pub fn delete_relationship_by_id(conn: &Connection, relationship_id: i64) -> Result<(), AppError> {
246    conn.execute(
247        "DELETE FROM memory_relationships WHERE relationship_id = ?1",
248        params![relationship_id],
249    )?;
250    conn.execute(
251        "DELETE FROM relationships WHERE id = ?1",
252        params![relationship_id],
253    )?;
254    Ok(())
255}
256
257/// Recalculates the `degree` field of an entity.
258///
259/// # Errors
260///
261/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
262pub fn recalculate_degree(conn: &Connection, entity_id: i64) -> Result<(), AppError> {
263    conn.execute(
264        "UPDATE entities
265         SET degree = (SELECT COUNT(*) FROM relationships
266                       WHERE source_id = entities.id OR target_id = entities.id)
267         WHERE id = ?1",
268        params![entity_id],
269    )?;
270    Ok(())
271}
272
273/// Locates orphan entities: no link in memory_entities and no relations.
274///
275/// # Errors
276///
277/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
278pub fn find_orphan_entity_ids(
279    conn: &Connection,
280    namespace: Option<&str>,
281) -> Result<Vec<i64>, AppError> {
282    if let Some(ns) = namespace {
283        let mut stmt = conn.prepare_cached(
284            "SELECT e.id FROM entities e
285             WHERE e.namespace = ?1
286               AND NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
287               AND NOT EXISTS (
288                   SELECT 1 FROM relationships r
289                   WHERE r.source_id = e.id OR r.target_id = e.id
290               )",
291        )?;
292        let ids = stmt
293            .query_map(params![ns], |r| r.get::<_, i64>(0))?
294            .collect::<Result<Vec<_>, _>>()?;
295        Ok(ids)
296    } else {
297        let mut stmt = conn.prepare_cached(
298            "SELECT e.id FROM entities e
299             WHERE NOT EXISTS (SELECT 1 FROM memory_entities me WHERE me.entity_id = e.id)
300               AND NOT EXISTS (
301                   SELECT 1 FROM relationships r
302                   WHERE r.source_id = e.id OR r.target_id = e.id
303               )",
304        )?;
305        let ids = stmt
306            .query_map([], |r| r.get::<_, i64>(0))?
307            .collect::<Result<Vec<_>, _>>()?;
308        Ok(ids)
309    }
310}
311
312/// Finds relationship rows whose `source_id` or `target_id` has no entity.
313///
314/// Distinct from `find_orphan_entity_ids`, which finds entities carrying no
315/// edges. This is the mirror image: edges carrying no entity. Both are called
316/// "orphans" and only the first had a repair path, so a database holding a
317/// dangling edge had no supported way to clean it — while `PRAGMA
318/// foreign_key_check` reported it on every migration, and `ensure_db_ready`
319/// migrates on open.
320///
321/// Such rows cannot be written while enforcement is on. They exist in files
322/// that predate it, or that were written while `PRAGMA foreign_keys = OFF` was
323/// in effect for a schema rebuild.
324///
325/// Namespace-scoped through the relationship's own column, so repairing one
326/// project never reaches another's edges.
327///
328/// # Errors
329///
330/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
331pub fn find_dangling_relationship_ids(
332    conn: &Connection,
333    namespace: Option<&str>,
334) -> Result<Vec<i64>, AppError> {
335    let mut stmt = conn.prepare_cached(
336        "SELECT r.id FROM relationships r
337         WHERE (?1 IS NULL OR r.namespace = ?1)
338           AND (NOT EXISTS (SELECT 1 FROM entities e WHERE e.id = r.source_id)
339             OR NOT EXISTS (SELECT 1 FROM entities e WHERE e.id = r.target_id))",
340    )?;
341    let ids = stmt
342        .query_map(params![namespace], |r| r.get::<_, i64>(0))?
343        .collect::<Result<Vec<_>, _>>()?;
344    Ok(ids)
345}
346
347/// Deletes relationship rows by primary key. Returns how many were removed.
348///
349/// # Errors
350///
351/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
352pub fn delete_relationships_by_ids(
353    conn: &Connection,
354    relationship_ids: &[i64],
355) -> Result<usize, AppError> {
356    let mut removed = 0usize;
357    for id in relationship_ids {
358        removed += conn.execute("DELETE FROM relationships WHERE id = ?1", params![id])?;
359    }
360    Ok(removed)
361}
362
363/// Deletes entities and their associated vectors. Returns the number of entities removed.
364///
365/// # Errors
366///
367/// Returns [`AppError::Database`] when the underlying SQLite operation fails.
368pub fn delete_entities_by_ids(conn: &Connection, entity_ids: &[i64]) -> Result<usize, AppError> {
369    if entity_ids.is_empty() {
370        return Ok(0);
371    }
372    let mut removed = 0usize;
373    for id in entity_ids {
374        // FK CASCADE on entity_embeddings handles cleanup automatically.
375        let _ = conn.execute("DELETE FROM vec_entities WHERE entity_id = ?1", params![id]);
376        let affected = conn.execute("DELETE FROM entities WHERE id = ?1", params![id])?;
377        removed += affected;
378    }
379    Ok(removed)
380}
381
382/// Counts relationships matching the given relation type within a namespace.
383///
384/// Used by `prune-relations --dry-run` to preview the number of relationships
385/// that would be deleted without actually modifying the database.
386///
387/// # Errors
388///
389/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
390pub fn count_relationships_by_relation(
391    conn: &Connection,
392    namespace: &str,
393    relation: &str,
394) -> Result<usize, AppError> {
395    let count: i64 = conn.query_row(
396        "SELECT COUNT(*) FROM relationships WHERE namespace = ?1 AND relation = ?2",
397        params![namespace, relation],
398        |r| r.get(0),
399    )?;
400    Ok(count as usize)
401}
402
403/// Returns unique entity names involved in relationships of the given type.
404///
405/// Queries both source and target sides of every matching relationship row,
406/// deduplicates via `DISTINCT`, and returns the names in alphabetical order.
407///
408/// # Errors
409///
410/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
411pub fn list_entity_names_by_relation(
412    conn: &Connection,
413    namespace: &str,
414    relation: &str,
415) -> Result<Vec<String>, AppError> {
416    let mut stmt = conn.prepare_cached(
417        "SELECT DISTINCT e.name FROM entities e
418         INNER JOIN relationships r ON (e.id = r.source_id OR e.id = r.target_id)
419         WHERE r.namespace = ?1 AND r.relation = ?2
420         ORDER BY e.name",
421    )?;
422    let names: Vec<String> = stmt
423        .query_map(params![namespace, relation], |row| row.get(0))?
424        .collect::<Result<Vec<_>, _>>()?;
425    Ok(names)
426}
427
428/// Deletes all relationships matching a relation type within a namespace.
429///
430/// Operates in chunks of 1000 to avoid holding long write locks and blocking
431/// WAL readers. After deletion, recalculates degree for every affected entity.
432///
433/// Returns `(count_deleted, affected_entity_ids)`.
434///
435/// # Errors
436///
437/// Returns `Err(AppError::Database)` on any `rusqlite` failure.
438pub fn delete_relationships_by_relation(
439    conn: &Connection,
440    namespace: &str,
441    relation: &str,
442) -> Result<(usize, Vec<i64>), AppError> {
443    // Step 1: collect all affected entity IDs before deletion.
444    let mut stmt = conn.prepare_cached(
445        "SELECT DISTINCT source_id FROM relationships WHERE namespace = ?1 AND relation = ?2
446         UNION
447         SELECT DISTINCT target_id FROM relationships WHERE namespace = ?1 AND relation = ?2",
448    )?;
449    let entity_ids: Vec<i64> = stmt
450        .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
451        .collect::<Result<Vec<_>, _>>()?;
452
453    // Step 2: collect relationship IDs to delete.
454    let mut id_stmt =
455        conn.prepare_cached("SELECT id FROM relationships WHERE namespace = ?1 AND relation = ?2")?;
456    let rel_ids: Vec<i64> = id_stmt
457        .query_map(params![namespace, relation], |r| r.get::<_, i64>(0))?
458        .collect::<Result<Vec<_>, _>>()?;
459
460    // Step 3: delete in chunks of 1000 (memory_relationships + relationships).
461    let mut total_deleted: usize = 0;
462    for chunk in rel_ids.chunks(1000) {
463        for &rel_id in chunk {
464            conn.execute(
465                "DELETE FROM memory_relationships WHERE relationship_id = ?1",
466                params![rel_id],
467            )?;
468            let affected =
469                conn.execute("DELETE FROM relationships WHERE id = ?1", params![rel_id])?;
470            total_deleted += affected;
471        }
472    }
473
474    // Step 4: recalculate degree for all affected entities.
475    for &eid in &entity_ids {
476        recalculate_degree(conn, eid)?;
477    }
478
479    Ok((total_deleted, entity_ids))
480}