sqlite_graphrag/commands/
delete_entity.rs1use crate::errors::AppError;
8use crate::i18n::errors_msg;
9use crate::output::{self, OutputFormat};
10use crate::paths::AppPaths;
11use crate::storage::connection::open_rw;
12use crate::storage::entities;
13use rusqlite::params;
14use serde::Serialize;
15
16#[derive(clap::Args)]
17#[command(after_long_help = "EXAMPLES:\n \
18 # Delete an entity and all its relationships (cascade required)\n \
19 sqlite-graphrag delete-entity --name auth-module --cascade\n\n \
20 # Delete an entity in a specific namespace\n \
21 sqlite-graphrag delete-entity --name legacy-service --cascade --namespace my-project\n\n \
22 # Without --cascade the command exits with an error:\n \
23 sqlite-graphrag delete-entity --name auth-module\n \
24 # => Error: use --cascade to confirm deletion of entity and all its relationships\n\n\
25NOTE:\n \
26 --cascade is required and acts as an explicit confirmation gate.\n \
27 All relationships where this entity is source or target are removed.\n \
28 All memory-entity bindings (memory_entities rows) are also removed.\n \
29 Run `sqlite-graphrag cleanup-orphans` afterwards to remove any newly orphaned entities.")]
30pub struct DeleteEntityArgs {
31 #[arg(long)]
33 pub name: String,
34 #[arg(long, default_value_t = false)]
38 pub cascade: bool,
39 #[arg(long)]
40 pub namespace: Option<String>,
41 #[arg(long, value_enum, default_value = "json")]
42 pub format: OutputFormat,
43 #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
44 pub json: bool,
45 #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
46 pub db: Option<String>,
47}
48
49#[derive(Serialize)]
50struct DeleteEntityResponse {
51 action: String,
52 entity_name: String,
53 namespace: String,
54 relationships_removed: usize,
55 bindings_removed: usize,
56 elapsed_ms: u64,
58}
59
60pub fn run(args: DeleteEntityArgs) -> Result<(), AppError> {
61 let inicio = std::time::Instant::now();
62
63 if !args.cascade {
64 return Err(AppError::Validation(
65 "use --cascade to confirm deletion of entity and all its relationships".to_string(),
66 ));
67 }
68
69 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
70 let paths = AppPaths::resolve(args.db.as_deref())?;
71
72 crate::storage::connection::ensure_db_ready(&paths)?;
73
74 let mut conn = open_rw(&paths.db)?;
75
76 let entity_id = entities::find_entity_id(&conn, &namespace, &args.name)?
77 .ok_or_else(|| AppError::NotFound(errors_msg::entity_not_found(&args.name, &namespace)))?;
78
79 let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
80
81 let adjacent_ids: Vec<i64> = {
83 let mut stmt = tx.prepare(
84 "SELECT DISTINCT CASE WHEN source_id = ?1 THEN target_id ELSE source_id END
85 FROM relationships WHERE source_id = ?1 OR target_id = ?1",
86 )?;
87 let ids: Vec<i64> = stmt
88 .query_map(params![entity_id], |r| r.get(0))?
89 .collect::<Result<Vec<_>, _>>()?;
90 ids
91 };
92
93 let rel_ids: Vec<i64> = {
95 let mut stmt =
96 tx.prepare("SELECT id FROM relationships WHERE source_id = ?1 OR target_id = ?1")?;
97 let ids: Vec<i64> = stmt
98 .query_map(params![entity_id], |r| r.get::<_, i64>(0))?
99 .collect::<Result<Vec<_>, _>>()?;
100 ids
101 };
102
103 for &rel_id in &rel_ids {
105 tx.execute(
106 "DELETE FROM memory_relationships WHERE relationship_id = ?1",
107 params![rel_id],
108 )?;
109 }
110
111 let relationships_removed = tx.execute(
113 "DELETE FROM relationships WHERE source_id = ?1 OR target_id = ?1",
114 params![entity_id],
115 )?;
116
117 let bindings_removed = tx.execute(
119 "DELETE FROM memory_entities WHERE entity_id = ?1",
120 params![entity_id],
121 )?;
122
123 let _ = tx.execute(
125 "DELETE FROM vec_entities WHERE entity_id = ?1",
126 params![entity_id],
127 );
128
129 tx.execute("DELETE FROM entities WHERE id = ?1", params![entity_id])?;
131
132 for &adj_id in &adjacent_ids {
134 if adj_id != entity_id {
135 entities::recalculate_degree(&tx, adj_id)?;
136 }
137 }
138
139 tx.commit()?;
140
141 conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
142
143 let response = DeleteEntityResponse {
144 action: "deleted".to_string(),
145 entity_name: args.name.clone(),
146 namespace: namespace.clone(),
147 relationships_removed,
148 bindings_removed,
149 elapsed_ms: inicio.elapsed().as_millis() as u64,
150 };
151
152 match args.format {
153 OutputFormat::Json => output::emit_json(&response)?,
154 OutputFormat::Text | OutputFormat::Markdown => {
155 output::emit_text(&format!(
156 "deleted: {} (relationships_removed={}, bindings_removed={}) [{}]",
157 response.entity_name,
158 response.relationships_removed,
159 response.bindings_removed,
160 response.namespace
161 ));
162 }
163 }
164
165 Ok(())
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn delete_entity_response_serializes_all_fields() {
174 let resp = DeleteEntityResponse {
175 action: "deleted".to_string(),
176 entity_name: "auth-module".to_string(),
177 namespace: "global".to_string(),
178 relationships_removed: 3,
179 bindings_removed: 2,
180 elapsed_ms: 7,
181 };
182 let json = serde_json::to_value(&resp).expect("serialization failed");
183 assert_eq!(json["action"], "deleted");
184 assert_eq!(json["entity_name"], "auth-module");
185 assert_eq!(json["namespace"], "global");
186 assert_eq!(json["relationships_removed"], 3);
187 assert_eq!(json["bindings_removed"], 2);
188 assert!(json["elapsed_ms"].is_number());
189 }
190
191 #[test]
192 fn delete_entity_response_action_is_deleted() {
193 let resp = DeleteEntityResponse {
194 action: "deleted".to_string(),
195 entity_name: "x".to_string(),
196 namespace: "ns".to_string(),
197 relationships_removed: 0,
198 bindings_removed: 0,
199 elapsed_ms: 0,
200 };
201 let json = serde_json::to_value(&resp).expect("serialization failed");
202 assert_eq!(json["action"], "deleted");
203 }
204
205 #[test]
206 fn delete_entity_response_zero_counts_allowed() {
207 let resp = DeleteEntityResponse {
208 action: "deleted".to_string(),
209 entity_name: "orphan-entity".to_string(),
210 namespace: "global".to_string(),
211 relationships_removed: 0,
212 bindings_removed: 0,
213 elapsed_ms: 1,
214 };
215 let json = serde_json::to_value(&resp).expect("serialization failed");
216 assert_eq!(json["relationships_removed"], 0);
217 assert_eq!(json["bindings_removed"], 0);
218 }
219}