Skip to main content

sqlite_graphrag/commands/
edit.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, versions};
7use serde::Serialize;
8use std::io::Read as _;
9
10#[derive(clap::Args)]
11pub struct EditArgs {
12    #[arg(long)]
13    pub name: String,
14    #[arg(long)]
15    pub body: Option<String>,
16    #[arg(long)]
17    pub body_file: Option<std::path::PathBuf>,
18    #[arg(long)]
19    pub body_stdin: bool,
20    #[arg(long)]
21    pub description: Option<String>,
22    #[arg(
23        long,
24        value_name = "EPOCH_OR_RFC3339",
25        value_parser = crate::parsers::parse_expected_updated_at,
26        long_help = "Optimistic lock: reject if updated_at does not match. \
27Accepts Unix epoch (e.g. 1700000000) or RFC 3339 (e.g. 2026-04-19T12:00:00Z)."
28    )]
29    pub expected_updated_at: Option<i64>,
30    #[arg(long, default_value = "global")]
31    pub namespace: Option<String>,
32    #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
33    pub json: bool,
34    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
35    pub db: Option<String>,
36}
37
38#[derive(Serialize)]
39struct EditResponse {
40    memory_id: i64,
41    name: String,
42    action: String,
43    version: i64,
44    /// Tempo total de execução em milissegundos desde início do handler até serialização.
45    elapsed_ms: u64,
46}
47
48pub fn run(args: EditArgs) -> Result<(), AppError> {
49    use crate::constants::*;
50
51    let inicio = std::time::Instant::now();
52    let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
53
54    let paths = AppPaths::resolve(args.db.as_deref())?;
55    let mut conn = open_rw(&paths.db)?;
56
57    let (memory_id, current_updated_at, _current_version) =
58        memories::find_by_name(&conn, &namespace, &args.name)?.ok_or_else(|| {
59            AppError::NotFound(erros::memoria_nao_encontrada(&args.name, &namespace))
60        })?;
61
62    if let Some(expected) = args.expected_updated_at {
63        if expected != current_updated_at {
64            return Err(AppError::Conflict(erros::conflito_optimistic_lock(
65                expected,
66                current_updated_at,
67            )));
68        }
69    }
70
71    let mut raw_body: Option<String> = None;
72    if args.body.is_some() || args.body_file.is_some() || args.body_stdin {
73        let b = if let Some(b) = args.body {
74            b
75        } else if let Some(path) = &args.body_file {
76            std::fs::read_to_string(path).map_err(AppError::Io)?
77        } else {
78            let mut buf = String::new();
79            std::io::stdin()
80                .read_to_string(&mut buf)
81                .map_err(AppError::Io)?;
82            buf
83        };
84        if b.len() > MAX_MEMORY_BODY_LEN {
85            return Err(AppError::LimitExceeded(
86                crate::i18n::validacao::body_excede(MAX_MEMORY_BODY_LEN),
87            ));
88        }
89        raw_body = Some(b);
90    }
91
92    if let Some(ref desc) = args.description {
93        if desc.len() > MAX_MEMORY_DESCRIPTION_LEN {
94            return Err(AppError::Validation(
95                crate::i18n::validacao::descricao_excede(MAX_MEMORY_DESCRIPTION_LEN),
96            ));
97        }
98    }
99
100    let row = memories::read_by_name(&conn, &namespace, &args.name)?
101        .ok_or_else(|| AppError::Internal(anyhow::anyhow!("memory row not found after check")))?;
102
103    let new_body = raw_body.unwrap_or(row.body.clone());
104    let new_description = args.description.unwrap_or(row.description.clone());
105    let new_hash = blake3::hash(new_body.as_bytes()).to_hex().to_string();
106    let memory_type = row.memory_type.clone();
107    let metadata = row.metadata.clone();
108
109    let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
110
111    let affected = if let Some(ts) = args.expected_updated_at {
112        tx.execute(
113            "UPDATE memories SET description=?2, body=?3, body_hash=?4
114             WHERE id=?1 AND updated_at=?5 AND deleted_at IS NULL",
115            rusqlite::params![memory_id, new_description, new_body, new_hash, ts],
116        )?
117    } else {
118        tx.execute(
119            "UPDATE memories SET description=?2, body=?3, body_hash=?4
120             WHERE id=?1 AND deleted_at IS NULL",
121            rusqlite::params![memory_id, new_description, new_body, new_hash],
122        )?
123    };
124
125    if affected == 0 {
126        return Err(AppError::Conflict(
127            "optimistic lock conflict: memory was modified by another process".to_string(),
128        ));
129    }
130
131    let next_v = versions::next_version(&tx, memory_id)?;
132
133    versions::insert_version(
134        &tx,
135        memory_id,
136        next_v,
137        &args.name,
138        &memory_type,
139        &new_description,
140        &new_body,
141        &metadata,
142        None,
143        "edit",
144    )?;
145
146    tx.commit()?;
147
148    output::emit_json(&EditResponse {
149        memory_id,
150        name: args.name,
151        action: "updated".to_string(),
152        version: next_v,
153        elapsed_ms: inicio.elapsed().as_millis() as u64,
154    })?;
155
156    Ok(())
157}
158
159#[cfg(test)]
160mod testes {
161    use super::*;
162
163    #[test]
164    fn edit_response_serializa_todos_campos() {
165        let resp = EditResponse {
166            memory_id: 42,
167            name: "minha-memoria".to_string(),
168            action: "updated".to_string(),
169            version: 3,
170            elapsed_ms: 7,
171        };
172        let json = serde_json::to_value(&resp).expect("serialização falhou");
173        assert_eq!(json["memory_id"], 42i64);
174        assert_eq!(json["name"], "minha-memoria");
175        assert_eq!(json["action"], "updated");
176        assert_eq!(json["version"], 3i64);
177        assert!(json["elapsed_ms"].is_number());
178    }
179
180    #[test]
181    fn edit_response_action_contem_updated() {
182        let resp = EditResponse {
183            memory_id: 1,
184            name: "n".to_string(),
185            action: "updated".to_string(),
186            version: 1,
187            elapsed_ms: 0,
188        };
189        assert_eq!(
190            resp.action, "updated",
191            "action deve ser 'updated' para edições bem-sucedidas"
192        );
193    }
194
195    #[test]
196    fn edit_body_excede_limite_retorna_erro() {
197        let limite = crate::constants::MAX_MEMORY_BODY_LEN;
198        let corpo_grande: String = "a".repeat(limite + 1);
199        assert!(
200            corpo_grande.len() > limite,
201            "corpo acima do limite deve ter tamanho > MAX_MEMORY_BODY_LEN"
202        );
203    }
204
205    #[test]
206    fn edit_description_excede_limite_retorna_erro() {
207        let limite = crate::constants::MAX_MEMORY_DESCRIPTION_LEN;
208        let desc_grande: String = "d".repeat(limite + 1);
209        assert!(
210            desc_grande.len() > limite,
211            "descrição acima do limite deve ter tamanho > MAX_MEMORY_DESCRIPTION_LEN"
212        );
213    }
214}