sqlite_graphrag/commands/
restore.rs1use crate::errors::AppError;
2use crate::i18n::erros;
3use crate::output;
4use crate::output::JsonOutputFormat;
5use crate::paths::AppPaths;
6use crate::storage::connection::open_rw;
7use crate::storage::versions;
8use rusqlite::params;
9use rusqlite::OptionalExtension;
10use serde::Serialize;
11
12#[derive(clap::Args)]
13pub struct RestoreArgs {
14 #[arg(long)]
15 pub name: String,
16 #[arg(long)]
17 pub version: i64,
18 #[arg(long, default_value = "global")]
19 pub namespace: Option<String>,
20 #[arg(
22 long,
23 value_name = "EPOCH_OR_RFC3339",
24 value_parser = crate::parsers::parse_expected_updated_at,
25 long_help = "Optimistic lock: reject if updated_at does not match. \
26Accepts Unix epoch (e.g. 1700000000) or RFC 3339 (e.g. 2026-04-19T12:00:00Z)."
27 )]
28 pub expected_updated_at: Option<i64>,
29 #[arg(long, value_enum, default_value_t = JsonOutputFormat::Json)]
31 pub format: JsonOutputFormat,
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 RestoreResponse {
40 memory_id: i64,
41 name: String,
42 version: i64,
43 restored_from: i64,
44 elapsed_ms: u64,
46}
47
48pub fn run(args: RestoreArgs) -> Result<(), AppError> {
49 let inicio = std::time::Instant::now();
50 let _ = args.format;
51 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
52 let paths = AppPaths::resolve(args.db.as_deref())?;
53 let mut conn = open_rw(&paths.db)?;
54
55 let result: Option<(i64, i64)> = conn
57 .query_row(
58 "SELECT id, updated_at FROM memories WHERE namespace = ?1 AND name = ?2",
59 params![namespace, args.name],
60 |r| Ok((r.get(0)?, r.get(1)?)),
61 )
62 .optional()?;
63 let (memory_id, current_updated_at) = result
64 .ok_or_else(|| AppError::NotFound(erros::memoria_nao_encontrada(&args.name, &namespace)))?;
65
66 if let Some(expected) = args.expected_updated_at {
67 if expected != current_updated_at {
68 return Err(AppError::Conflict(erros::conflito_optimistic_lock(
69 expected,
70 current_updated_at,
71 )));
72 }
73 }
74
75 let version_row: (String, String, String, String, String) = {
76 let mut stmt = conn.prepare(
77 "SELECT name, type, description, body, metadata
78 FROM memory_versions
79 WHERE memory_id = ?1 AND version = ?2",
80 )?;
81
82 stmt.query_row(params![memory_id, args.version], |r| {
83 Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?))
84 })
85 .map_err(|_| AppError::NotFound(erros::versao_nao_encontrada(args.version, &args.name)))?
86 };
87
88 let (old_name, old_type, old_description, old_body, old_metadata) = version_row;
89
90 let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
91
92 let affected = if let Some(ts) = args.expected_updated_at {
94 tx.execute(
95 "UPDATE memories SET name=?2, type=?3, description=?4, body=?5, body_hash=?6, deleted_at=NULL
96 WHERE id=?1 AND updated_at=?7",
97 rusqlite::params![
98 memory_id,
99 old_name,
100 old_type,
101 old_description,
102 old_body,
103 blake3::hash(old_body.as_bytes()).to_hex().to_string(),
104 ts
105 ],
106 )?
107 } else {
108 tx.execute(
109 "UPDATE memories SET name=?2, type=?3, description=?4, body=?5, body_hash=?6, deleted_at=NULL
110 WHERE id=?1",
111 rusqlite::params![
112 memory_id,
113 old_name,
114 old_type,
115 old_description,
116 old_body,
117 blake3::hash(old_body.as_bytes()).to_hex().to_string()
118 ],
119 )?
120 };
121
122 if affected == 0 {
123 return Err(AppError::Conflict(erros::conflito_processo_concorrente()));
124 }
125
126 let next_v = versions::next_version(&tx, memory_id)?;
127
128 versions::insert_version(
129 &tx,
130 memory_id,
131 next_v,
132 &old_name,
133 &old_type,
134 &old_description,
135 &old_body,
136 &old_metadata,
137 None,
138 "restore",
139 )?;
140
141 tx.commit()?;
142
143 output::emit_json(&RestoreResponse {
144 memory_id,
145 name: old_name,
146 version: next_v,
147 restored_from: args.version,
148 elapsed_ms: inicio.elapsed().as_millis() as u64,
149 })?;
150
151 Ok(())
152}
153
154#[cfg(test)]
155mod testes {
156 use crate::errors::AppError;
157
158 #[test]
159 fn optimistic_lock_conflict_retorna_exit_3() {
160 let err = AppError::Conflict(
161 "optimistic lock conflict: expected updated_at=50, but current is 99".to_string(),
162 );
163 assert_eq!(err.exit_code(), 3);
164 assert!(err.to_string().contains("conflict"));
165 }
166}