Skip to main content

sqlite_graphrag/commands/
cleanup_orphans.rs

1use crate::errors::AppError;
2use crate::i18n::erros;
3use crate::output::{self, OutputFormat};
4use crate::paths::AppPaths;
5use crate::storage::connection::open_rw;
6use crate::storage::entities;
7use serde::Serialize;
8
9#[derive(clap::Args)]
10pub struct CleanupOrphansArgs {
11    #[arg(long)]
12    pub namespace: Option<String>,
13    #[arg(long)]
14    pub dry_run: bool,
15    #[arg(long)]
16    pub yes: bool,
17    #[arg(long, value_enum, default_value = "json")]
18    pub format: OutputFormat,
19    #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
20    pub json: bool,
21    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
22    pub db: Option<String>,
23}
24
25#[derive(Serialize)]
26struct CleanupResponse {
27    orphan_count: usize,
28    deleted: usize,
29    dry_run: bool,
30    namespace: Option<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: CleanupOrphansArgs) -> Result<(), AppError> {
36    let inicio = std::time::Instant::now();
37    let paths = AppPaths::resolve(args.db.as_deref())?;
38
39    if !paths.db.exists() {
40        return Err(AppError::NotFound(erros::banco_nao_encontrado(
41            &paths.db.display().to_string(),
42        )));
43    }
44
45    let mut conn = open_rw(&paths.db)?;
46
47    let orphan_ids = entities::find_orphan_entity_ids(&conn, args.namespace.as_deref())?;
48    let orphan_count = orphan_ids.len();
49
50    let deleted = if args.dry_run {
51        0
52    } else {
53        if orphan_count > 0 && !args.yes {
54            output::emit_progress(&format!(
55                "removing {orphan_count} orphan entities (use --yes to skip this notice)"
56            ));
57        }
58        let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
59        let removed = entities::delete_entities_by_ids(&tx, &orphan_ids)?;
60        tx.commit()?;
61        removed
62    };
63
64    let response = CleanupResponse {
65        orphan_count,
66        deleted,
67        dry_run: args.dry_run,
68        namespace: args.namespace.clone(),
69        elapsed_ms: inicio.elapsed().as_millis() as u64,
70    };
71
72    match args.format {
73        OutputFormat::Json => output::emit_json(&response)?,
74        OutputFormat::Text | OutputFormat::Markdown => {
75            let ns = response.namespace.as_deref().unwrap_or("<all>");
76            output::emit_text(&format!(
77                "orphans: {} found, {} deleted (dry_run={}) [{}]",
78                response.orphan_count, response.deleted, response.dry_run, ns
79            ));
80        }
81    }
82
83    Ok(())
84}
85
86#[cfg(test)]
87mod testes {
88    use super::*;
89
90    #[test]
91    fn cleanup_response_serializa_dry_run_true() {
92        let resp = CleanupResponse {
93            orphan_count: 5,
94            deleted: 0,
95            dry_run: true,
96            namespace: Some("global".to_string()),
97            elapsed_ms: 12,
98        };
99        let json = serde_json::to_value(&resp).expect("serialização falhou");
100        assert_eq!(json["orphan_count"], 5);
101        assert_eq!(json["deleted"], 0);
102        assert_eq!(json["dry_run"], true);
103        assert_eq!(json["namespace"], "global");
104        assert!(json["elapsed_ms"].is_number());
105    }
106
107    #[test]
108    fn cleanup_response_deleted_zero_quando_dry_run() {
109        let resp = CleanupResponse {
110            orphan_count: 10,
111            deleted: 0,
112            dry_run: true,
113            namespace: None,
114            elapsed_ms: 5,
115        };
116        assert_eq!(resp.deleted, 0, "dry_run deve manter deleted em 0");
117        assert_eq!(resp.orphan_count, 10);
118    }
119
120    #[test]
121    fn cleanup_response_namespace_none_serializa_null() {
122        let resp = CleanupResponse {
123            orphan_count: 0,
124            deleted: 0,
125            dry_run: false,
126            namespace: None,
127            elapsed_ms: 1,
128        };
129        let json = serde_json::to_value(&resp).expect("serialização falhou");
130        assert!(
131            json["namespace"].is_null(),
132            "namespace None deve serializar como null"
133        );
134    }
135
136    #[test]
137    fn cleanup_response_deleted_igual_orphan_count_quando_executado() {
138        let resp = CleanupResponse {
139            orphan_count: 3,
140            deleted: 3,
141            dry_run: false,
142            namespace: Some("projeto".to_string()),
143            elapsed_ms: 20,
144        };
145        assert_eq!(
146            resp.deleted, resp.orphan_count,
147            "ao executar sem dry_run, deleted deve igualar orphan_count"
148        );
149    }
150}