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