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 as a positional argument. Alternative to `--name`.
12    #[arg(value_name = "NAME", conflicts_with = "name")]
13    pub name_positional: Option<String>,
14    /// Memory name to soft-delete. The row is preserved with `deleted_at` set, recoverable via `restore`.
15    /// Use `purge` to permanently remove soft-deleted memories.
16    #[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    /// Tempo total de execução em milissegundos desde início do handler até serialização.
32    elapsed_ms: u64,
33}
34
35pub fn run(args: ForgetArgs) -> Result<(), AppError> {
36    let inicio = std::time::Instant::now();
37    // Resolve name from positional or --name flag; both are optional, at least one is required.
38    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        // FTS5 external-content: manual `DELETE FROM fts_memories WHERE rowid=?`
62        // corrompe o índice. A limpeza correta acontece via trigger `trg_fts_ad`
63        // quando `purge` remove fisicamente a linha de `memories`. Entre soft-delete
64        // e purge, as queries FTS filtram `m.deleted_at IS NULL` no JOIN.
65        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}