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