sqlite_graphrag/commands/
sync_safe_copy.rs1use crate::errors::AppError;
4use crate::i18n::{errors_msg, validation};
5use crate::output;
6use crate::paths::AppPaths;
7use crate::storage::connection::open_rw;
8use serde::Serialize;
9
10#[derive(clap::Args)]
11pub struct SyncSafeCopyArgs {
12 #[arg(long, alias = "to", alias = "output")]
14 pub dest: std::path::PathBuf,
15 #[arg(long, help = "No-op; JSON is always emitted on stdout")]
16 pub json: bool,
17 #[arg(long, value_parser = ["json", "text"], hide = true)]
19 pub format: Option<String>,
20 #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
21 pub db: Option<String>,
22}
23
24#[derive(Serialize)]
25struct SyncSafeCopyResponse {
26 source_db_path: String,
27 dest_path: String,
28 bytes_copied: u64,
29 status: String,
30 elapsed_ms: u64,
32}
33
34pub fn run(args: SyncSafeCopyArgs) -> Result<(), AppError> {
35 let inicio = std::time::Instant::now();
36 let _ = args.format; let paths = AppPaths::resolve(args.db.as_deref())?;
38
39 if !paths.db.exists() {
40 return Err(AppError::NotFound(errors_msg::database_not_found(
41 &paths.db.display().to_string(),
42 )));
43 }
44
45 if args.dest == paths.db {
46 return Err(AppError::Validation(
47 validation::sync_destination_equals_source(),
48 ));
49 }
50
51 if let Some(parent) = args.dest.parent() {
52 std::fs::create_dir_all(parent)?;
53 }
54
55 let conn = open_rw(&paths.db)?;
56 conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
57 drop(conn);
58
59 let bytes_copied = std::fs::copy(&paths.db, &args.dest)?;
60
61 #[cfg(unix)]
63 {
64 use std::os::unix::fs::PermissionsExt;
65 let mut perms = std::fs::metadata(&args.dest)?.permissions();
66 perms.set_mode(0o600);
67 std::fs::set_permissions(&args.dest, perms)?;
68 }
69
70 output::emit_json(&SyncSafeCopyResponse {
71 source_db_path: paths.db.display().to_string(),
72 dest_path: args.dest.display().to_string(),
73 bytes_copied,
74 status: "ok".to_string(),
75 elapsed_ms: inicio.elapsed().as_millis() as u64,
76 })?;
77
78 Ok(())
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn sync_safe_copy_response_serializa_todos_campos() {
87 let resp = SyncSafeCopyResponse {
88 source_db_path: "/home/user/.local/share/sqlite-graphrag/db.sqlite".to_string(),
89 dest_path: "/tmp/backup.sqlite".to_string(),
90 bytes_copied: 16384,
91 status: "ok".to_string(),
92 elapsed_ms: 12,
93 };
94 let json = serde_json::to_value(&resp).expect("serialização falhou");
95 assert_eq!(
96 json["source_db_path"],
97 "/home/user/.local/share/sqlite-graphrag/db.sqlite"
98 );
99 assert_eq!(json["dest_path"], "/tmp/backup.sqlite");
100 assert_eq!(json["bytes_copied"], 16384u64);
101 assert_eq!(json["status"], "ok");
102 assert_eq!(json["elapsed_ms"], 12u64);
103 }
104
105 #[test]
106 fn sync_safe_copy_rejeita_dest_igual_source() {
107 let db_path = std::path::PathBuf::from("/tmp/mesmo.sqlite");
108 let args = SyncSafeCopyArgs {
109 dest: db_path.clone(),
110 json: false,
111 format: None,
112 db: Some("/tmp/mesmo.sqlite".to_string()),
113 };
114 let resultado = if args.dest == std::path::PathBuf::from(args.db.as_deref().unwrap_or("")) {
116 Err(AppError::Validation(
117 "destination path must differ from the source database path".to_string(),
118 ))
119 } else {
120 Ok(())
121 };
122 assert!(resultado.is_err(), "deve rejeitar dest igual ao source");
123 if let Err(AppError::Validation(msg)) = resultado {
124 assert!(msg.contains("destination path must differ"));
125 }
126 }
127
128 #[test]
129 fn sync_safe_copy_response_status_ok() {
130 let resp = SyncSafeCopyResponse {
131 source_db_path: "/data/db.sqlite".to_string(),
132 dest_path: "/backup/db.sqlite".to_string(),
133 bytes_copied: 0,
134 status: "ok".to_string(),
135 elapsed_ms: 0,
136 };
137 let json = serde_json::to_value(&resp).expect("serialização falhou");
138 assert_eq!(json["status"], "ok");
139 }
140
141 #[test]
142 fn sync_safe_copy_response_bytes_copied_zero_valido() {
143 let resp = SyncSafeCopyResponse {
144 source_db_path: "/data/db.sqlite".to_string(),
145 dest_path: "/backup/db.sqlite".to_string(),
146 bytes_copied: 0,
147 status: "ok".to_string(),
148 elapsed_ms: 1,
149 };
150 let json = serde_json::to_value(&resp).expect("serialização falhou");
151 assert_eq!(json["bytes_copied"], 0u64);
152 assert_eq!(json["elapsed_ms"], 1u64);
153 }
154}