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