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 {
32 #[arg(long)]
34 pub name: String,
35 #[arg(long, default_value_t = false)]
39 pub cascade: bool,
40 #[arg(long)]
42 pub namespace: Option<String>,
43 #[arg(long, value_enum, default_value = "json")]
45 pub format: OutputFormat,
46 #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
48 pub json: bool,
49 #[arg(long)]
51 pub db: Option<String>,
52}
53
54#[derive(Serialize)]
55struct DeleteEntityResponse {
56 action: String,
57 entity_name: String,
58 namespace: String,
59 relationships_removed: usize,
60 bindings_removed: usize,
61 elapsed_ms: u64,
63}
64
65pub fn run(args: DeleteEntityArgs) -> Result<(), AppError> {
67 let inicio = std::time::Instant::now();
68
69 if !args.cascade {
70 return Err(AppError::Validation(
71 "use --cascade to confirm deletion of entity and all its relationships".to_string(),
72 ));
73 }
74
75 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
76 let paths = AppPaths::resolve(args.db.as_deref())?;
77
78 crate::storage::connection::ensure_db_ready(&paths)?;
79
80 let mut conn = open_rw(&paths.db)?;
81
82 let entity_id = entities::find_entity_id(&conn, &namespace, &args.name)?
83 .ok_or_else(|| AppError::NotFound(errors_msg::entity_not_found(&args.name, &namespace)))?;
84
85 let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
86
87 let adjacent_ids: Vec<i64> = {
89 let mut stmt = tx.prepare(
90 "SELECT DISTINCT CASE WHEN source_id = ?1 THEN target_id ELSE source_id END
91 FROM relationships WHERE source_id = ?1 OR target_id = ?1",
92 )?;
93 let ids: Vec<i64> = stmt
94 .query_map(params![entity_id], |r| r.get(0))?
95 .collect::<Result<Vec<_>, _>>()?;
96 ids
97 };
98
99 let rel_ids: Vec<i64> = {
101 let mut stmt =
102 tx.prepare("SELECT id FROM relationships WHERE source_id = ?1 OR target_id = ?1")?;
103 let ids: Vec<i64> = stmt
104 .query_map(params![entity_id], |r| r.get::<_, i64>(0))?
105 .collect::<Result<Vec<_>, _>>()?;
106 ids
107 };
108
109 for &rel_id in &rel_ids {
111 tx.execute(
112 "DELETE FROM memory_relationships WHERE relationship_id = ?1",
113 params![rel_id],
114 )?;
115 }
116
117 let relationships_removed = tx.execute(
119 "DELETE FROM relationships WHERE source_id = ?1 OR target_id = ?1",
120 params![entity_id],
121 )?;
122
123 let bindings_removed = tx.execute(
125 "DELETE FROM memory_entities WHERE entity_id = ?1",
126 params![entity_id],
127 )?;
128
129 let _ = tx.execute(
131 "DELETE FROM vec_entities WHERE entity_id = ?1",
132 params![entity_id],
133 );
134
135 tx.execute("DELETE FROM entities WHERE id = ?1", params![entity_id])?;
137
138 for &adj_id in &adjacent_ids {
140 if adj_id != entity_id {
141 entities::recalculate_degree(&tx, adj_id)?;
142 }
143 }
144
145 tx.commit()?;
146
147 conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
148
149 let response = DeleteEntityResponse {
150 action: "deleted".to_string(),
151 entity_name: args.name.clone(),
152 namespace: namespace.clone(),
153 relationships_removed,
154 bindings_removed,
155 elapsed_ms: inicio.elapsed().as_millis() as u64,
156 };
157
158 match args.format {
159 OutputFormat::Json => output::emit_json(&response)?,
160 OutputFormat::Text | OutputFormat::Markdown => {
161 output::emit_text(&format!(
162 "deleted: {} (relationships_removed={}, bindings_removed={}) [{}]",
163 response.entity_name,
164 response.relationships_removed,
165 response.bindings_removed,
166 response.namespace
167 ));
168 }
169 }
170
171 Ok(())
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn delete_entity_response_serializes_all_fields() {
180 let resp = DeleteEntityResponse {
181 action: "deleted".to_string(),
182 entity_name: "auth-module".to_string(),
183 namespace: "global".to_string(),
184 relationships_removed: 3,
185 bindings_removed: 2,
186 elapsed_ms: 7,
187 };
188 let json = serde_json::to_value(&resp).expect("serialization failed");
189 assert_eq!(json["action"], "deleted");
190 assert_eq!(json["entity_name"], "auth-module");
191 assert_eq!(json["namespace"], "global");
192 assert_eq!(json["relationships_removed"], 3);
193 assert_eq!(json["bindings_removed"], 2);
194 assert!(json["elapsed_ms"].is_number());
195 }
196
197 #[test]
198 fn delete_entity_response_action_is_deleted() {
199 let resp = DeleteEntityResponse {
200 action: "deleted".to_string(),
201 entity_name: "x".to_string(),
202 namespace: "ns".to_string(),
203 relationships_removed: 0,
204 bindings_removed: 0,
205 elapsed_ms: 0,
206 };
207 let json = serde_json::to_value(&resp).expect("serialization failed");
208 assert_eq!(json["action"], "deleted");
209 }
210
211 #[test]
212 fn delete_entity_response_zero_counts_allowed() {
213 let resp = DeleteEntityResponse {
214 action: "deleted".to_string(),
215 entity_name: "orphan-entity".to_string(),
216 namespace: "global".to_string(),
217 relationships_removed: 0,
218 bindings_removed: 0,
219 elapsed_ms: 1,
220 };
221 let json = serde_json::to_value(&resp).expect("serialization failed");
222 assert_eq!(json["relationships_removed"], 0);
223 assert_eq!(json["bindings_removed"], 0);
224 }
225}