Skip to main content

sqlite_graphrag/commands/
forget.rs

1use 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    /// Memory name to soft-delete. The row is preserved with `deleted_at` set, recoverable via `restore`.
12    /// Use `purge` to permanently remove soft-deleted memories.
13    #[arg(long)]
14    pub name: String,
15    #[arg(long, default_value = "global")]
16    pub namespace: Option<String>,
17    #[arg(long, help = "No-op; JSON is always emitted on stdout")]
18    pub json: bool,
19    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
20    pub db: Option<String>,
21}
22
23#[derive(Serialize)]
24struct ForgetResponse {
25    forgotten: bool,
26    name: String,
27    namespace: String,
28    /// Tempo total de execução em milissegundos desde início do handler até serialização.
29    elapsed_ms: u64,
30}
31
32pub fn run(args: ForgetArgs) -> Result<(), AppError> {
33    let inicio = std::time::Instant::now();
34    let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
35    let paths = AppPaths::resolve(args.db.as_deref())?;
36    if !paths.db.exists() {
37        return Err(AppError::NotFound(erros::banco_nao_encontrado(
38            &paths.db.display().to_string(),
39        )));
40    }
41
42    let conn = open_rw(&paths.db)?;
43
44    let maybe_row = memories::read_by_name(&conn, &namespace, &args.name)?;
45    let forgotten = memories::soft_delete(&conn, &namespace, &args.name)?;
46
47    if !forgotten {
48        return Err(AppError::NotFound(erros::memoria_nao_encontrada(
49            &args.name, &namespace,
50        )));
51    }
52
53    if let Some(row) = maybe_row {
54        // FTS5 external-content: manual `DELETE FROM fts_memories WHERE rowid=?`
55        // corrompe o índice. A limpeza correta acontece via trigger `trg_fts_ad`
56        // quando `purge` remove fisicamente a linha de `memories`. Entre soft-delete
57        // e purge, as queries FTS filtram `m.deleted_at IS NULL` no JOIN.
58        if let Err(e) = memories::delete_vec(&conn, row.id) {
59            tracing::warn!(memory_id = row.id, error = %e, "vec cleanup failed — orphan vector left");
60        }
61    }
62
63    output::emit_json(&ForgetResponse {
64        forgotten: true,
65        name: args.name,
66        namespace,
67        elapsed_ms: inicio.elapsed().as_millis() as u64,
68    })?;
69
70    Ok(())
71}
72
73#[cfg(test)]
74mod testes {
75    use super::*;
76
77    #[test]
78    fn forget_response_serializa_campos_basicos() {
79        let resp = ForgetResponse {
80            forgotten: true,
81            name: "minha-memoria".to_string(),
82            namespace: "global".to_string(),
83            elapsed_ms: 5,
84        };
85        let json = serde_json::to_value(&resp).expect("serialização falhou");
86        assert_eq!(json["forgotten"], true);
87        assert_eq!(json["name"], "minha-memoria");
88        assert_eq!(json["namespace"], "global");
89        assert!(json["elapsed_ms"].is_number());
90    }
91
92    #[test]
93    fn forget_response_forgotten_true_indica_sucesso() {
94        let resp = ForgetResponse {
95            forgotten: true,
96            name: "teste".to_string(),
97            namespace: "ns".to_string(),
98            elapsed_ms: 1,
99        };
100        assert!(
101            resp.forgotten,
102            "forgotten deve ser true quando soft-delete bem-sucedido"
103        );
104    }
105
106    #[test]
107    fn forget_resposta_com_namespace_correto() {
108        let resp = ForgetResponse {
109            forgotten: true,
110            name: "abc".to_string(),
111            namespace: "meu-projeto".to_string(),
112            elapsed_ms: 0,
113        };
114        assert_eq!(
115            resp.namespace, "meu-projeto",
116            "namespace deve ser preservado na resposta"
117        );
118    }
119
120    #[test]
121    fn forget_elapsed_ms_zero_e_valido() {
122        let resp = ForgetResponse {
123            forgotten: true,
124            name: "qualquer".to_string(),
125            namespace: "global".to_string(),
126            elapsed_ms: 0,
127        };
128        let json = serde_json::to_value(&resp).expect("serialização falhou");
129        assert_eq!(json["elapsed_ms"], 0u64);
130    }
131}