sqlite_graphrag/commands/
forget.rs1use crate::errors::AppError;
4use crate::i18n::errors_msg;
5use crate::output;
6use crate::paths::AppPaths;
7use crate::storage::connection::open_rw;
8use crate::storage::memories;
9use serde::Serialize;
10
11#[derive(clap::Args)]
12pub struct ForgetArgs {
13 #[arg(value_name = "NAME", conflicts_with = "name")]
15 pub name_positional: Option<String>,
16 #[arg(long)]
19 pub name: Option<String>,
20 #[arg(long, default_value = "global")]
21 pub namespace: Option<String>,
22 #[arg(long, help = "No-op; JSON is always emitted on stdout")]
23 pub json: bool,
24 #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
25 pub db: Option<String>,
26}
27
28#[derive(Serialize)]
29struct ForgetResponse {
30 forgotten: bool,
31 name: String,
32 namespace: String,
33 elapsed_ms: u64,
35}
36
37pub fn run(args: ForgetArgs) -> Result<(), AppError> {
38 let inicio = std::time::Instant::now();
39 let name = args.name_positional.or(args.name).ok_or_else(|| {
41 AppError::Validation("name required: pass as positional argument or via --name".to_string())
42 })?;
43 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
44 let paths = AppPaths::resolve(args.db.as_deref())?;
45 if !paths.db.exists() {
46 return Err(AppError::NotFound(errors_msg::database_not_found(
47 &paths.db.display().to_string(),
48 )));
49 }
50
51 let conn = open_rw(&paths.db)?;
52
53 let maybe_row = memories::read_by_name(&conn, &namespace, &name)?;
54 let forgotten = memories::soft_delete(&conn, &namespace, &name)?;
55
56 if !forgotten {
57 return Err(AppError::NotFound(errors_msg::memory_not_found(
58 &name, &namespace,
59 )));
60 }
61
62 if let Some(row) = maybe_row {
63 if let Err(e) = memories::delete_vec(&conn, row.id) {
68 tracing::warn!(memory_id = row.id, error = %e, "vec cleanup failed — orphan vector left");
69 }
70 }
71
72 output::emit_json(&ForgetResponse {
73 forgotten: true,
74 name,
75 namespace,
76 elapsed_ms: inicio.elapsed().as_millis() as u64,
77 })?;
78
79 Ok(())
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn forget_response_serializa_campos_basicos() {
88 let resp = ForgetResponse {
89 forgotten: true,
90 name: "minha-memoria".to_string(),
91 namespace: "global".to_string(),
92 elapsed_ms: 5,
93 };
94 let json = serde_json::to_value(&resp).expect("serialização falhou");
95 assert_eq!(json["forgotten"], true);
96 assert_eq!(json["name"], "minha-memoria");
97 assert_eq!(json["namespace"], "global");
98 assert!(json["elapsed_ms"].is_number());
99 }
100
101 #[test]
102 fn forget_response_forgotten_true_indica_sucesso() {
103 let resp = ForgetResponse {
104 forgotten: true,
105 name: "teste".to_string(),
106 namespace: "ns".to_string(),
107 elapsed_ms: 1,
108 };
109 assert!(
110 resp.forgotten,
111 "forgotten deve ser true quando soft-delete bem-sucedido"
112 );
113 }
114
115 #[test]
116 fn forget_resposta_com_namespace_correto() {
117 let resp = ForgetResponse {
118 forgotten: true,
119 name: "abc".to_string(),
120 namespace: "meu-projeto".to_string(),
121 elapsed_ms: 0,
122 };
123 assert_eq!(
124 resp.namespace, "meu-projeto",
125 "namespace deve ser preservado na resposta"
126 );
127 }
128
129 #[test]
130 fn forget_elapsed_ms_zero_e_valido() {
131 let resp = ForgetResponse {
132 forgotten: true,
133 name: "qualquer".to_string(),
134 namespace: "global".to_string(),
135 elapsed_ms: 0,
136 };
137 let json = serde_json::to_value(&resp).expect("serialização falhou");
138 assert_eq!(json["elapsed_ms"], 0u64);
139 }
140}