1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::fs::{self, File};
9use std::io::{BufRead, BufReader, BufWriter, Write};
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TransactionLogEntry {
15 pub transaction_id: u64,
16 pub timestamp: DateTime<Utc>,
17 pub operation_type: OperationType,
18 pub data: Vec<u8>,
19 pub checksum: String,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub enum OperationType {
25 Insert,
26 Delete,
27 Update,
28 BeginTransaction,
29 CommitTransaction,
30 RollbackTransaction,
31}
32
33pub struct PitrConfig {
35 pub log_dir: PathBuf,
36 pub archive_dir: PathBuf,
37 pub max_log_size: u64,
38 pub auto_archive: bool,
39}
40
41pub struct TransactionLog {
43 config: PitrConfig,
44 current_log: Option<BufWriter<File>>,
45 next_transaction_id: u64,
46}
47
48impl TransactionLog {
49 pub fn new(config: PitrConfig) -> Result<Self, Box<dyn std::error::Error>> {
51 fs::create_dir_all(&config.log_dir)?;
53 fs::create_dir_all(&config.archive_dir)?;
54
55 let next_transaction_id = Self::get_next_transaction_id(&config.log_dir)?;
57
58 Ok(Self {
59 config,
60 current_log: None,
61 next_transaction_id,
62 })
63 }
64
65 pub fn append(
67 &mut self,
68 operation_type: OperationType,
69 data: Vec<u8>,
70 ) -> Result<u64, Box<dyn std::error::Error>> {
71 if self.current_log.is_none() {
73 self.open_current_log()?;
74 }
75
76 let transaction_id = self.next_transaction_id;
77 self.next_transaction_id += 1;
78
79 let entry = TransactionLogEntry {
80 transaction_id,
81 timestamp: Utc::now(),
82 operation_type,
83 data: data.clone(),
84 checksum: Self::calculate_checksum(&data),
85 };
86
87 let entry_json = serde_json::to_string(&entry)?;
89 if let Some(ref mut log) = self.current_log {
90 writeln!(log, "{}", entry_json)?;
91 log.flush()?;
92 }
93
94 if self.should_rotate_log()? {
96 self.rotate_log()?;
97 }
98
99 Ok(transaction_id)
100 }
101
102 pub fn recover_to_timestamp(
104 &self,
105 target_timestamp: DateTime<Utc>,
106 _output_dir: &Path,
107 ) -> Result<usize, Box<dyn std::error::Error>> {
108 println!("Starting Point-in-Time Recovery");
109 println!("Target timestamp: {}", target_timestamp.to_rfc3339());
110
111 let mut log_files = self.collect_log_files()?;
113 log_files.sort();
114
115 let mut applied_transactions = 0;
116
117 for log_file in log_files {
119 applied_transactions += self.replay_log_file(&log_file, Some(target_timestamp))?;
120 }
121
122 println!(
123 "Recovery complete: {} transactions applied",
124 applied_transactions
125 );
126 Ok(applied_transactions)
127 }
128
129 pub fn recover_to_transaction(
131 &self,
132 target_transaction_id: u64,
133 _output_dir: &Path,
134 ) -> Result<usize, Box<dyn std::error::Error>> {
135 println!("Starting Point-in-Time Recovery");
136 println!("Target transaction ID: {}", target_transaction_id);
137
138 let mut log_files = self.collect_log_files()?;
139 log_files.sort();
140
141 let mut applied_transactions = 0;
142
143 for log_file in log_files {
144 let file = File::open(&log_file)?;
145 let reader = BufReader::new(file);
146
147 for line in reader.lines() {
148 let line = line?;
149 if line.trim().is_empty() {
150 continue;
151 }
152
153 let entry: TransactionLogEntry = serde_json::from_str(&line)?;
154
155 if entry.transaction_id > target_transaction_id {
156 println!("Reached target transaction ID");
158 return Ok(applied_transactions);
159 }
160
161 if !Self::verify_checksum(&entry) {
163 eprintln!(
164 "Warning: Checksum mismatch for transaction {}",
165 entry.transaction_id
166 );
167 continue;
168 }
169
170 applied_transactions += 1;
172 }
173 }
174
175 Ok(applied_transactions)
176 }
177
178 pub fn create_checkpoint(&self, name: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
180 let checkpoint_file = self
181 .config
182 .archive_dir
183 .join(format!("checkpoint_{}.json", name));
184
185 let checkpoint = CheckpointMetadata {
186 name: name.to_string(),
187 timestamp: Utc::now(),
188 last_transaction_id: self.next_transaction_id - 1,
189 log_files: self.collect_log_files()?,
190 };
191
192 let mut file = File::create(&checkpoint_file)?;
193 serde_json::to_writer_pretty(&mut file, &checkpoint)?;
194
195 println!("Checkpoint created: {}", checkpoint_file.display());
196 Ok(checkpoint_file)
197 }
198
199 pub fn list_checkpoints(&self) -> Result<Vec<CheckpointMetadata>, Box<dyn std::error::Error>> {
201 let mut checkpoints: Vec<CheckpointMetadata> = Vec::new();
202
203 for entry in fs::read_dir(&self.config.archive_dir)? {
204 let entry = entry?;
205 let path = entry.path();
206
207 if path.extension().and_then(|s| s.to_str()) == Some("json")
208 && path
209 .file_name()
210 .and_then(|s| s.to_str())
211 .map(|s| s.starts_with("checkpoint_"))
212 .unwrap_or(false)
213 {
214 let file = File::open(&path)?;
215 if let Ok(checkpoint) = serde_json::from_reader(file) {
216 checkpoints.push(checkpoint);
217 }
218 }
219 }
220
221 checkpoints.sort_by_key(|item| item.timestamp);
222 Ok(checkpoints)
223 }
224
225 pub fn archive_logs(&mut self) -> Result<usize, Box<dyn std::error::Error>> {
227 self.close_current_log()?;
229
230 let mut archived = 0;
231
232 for entry in fs::read_dir(&self.config.log_dir)? {
234 let entry = entry?;
235 let path = entry.path();
236
237 if path.extension().and_then(|s| s.to_str()) == Some("wal") {
238 let filename = path.file_name().expect("file path should have a file name");
239 let archive_path = self.config.archive_dir.join(filename);
240
241 fs::rename(&path, &archive_path)?;
242 archived += 1;
243 }
244 }
245
246 println!("Archived {} log file(s)", archived);
247 Ok(archived)
248 }
249
250 fn open_current_log(&mut self) -> Result<(), Box<dyn std::error::Error>> {
253 let log_path = self
254 .config
255 .log_dir
256 .join(format!("transaction_{}.wal", Utc::now().timestamp()));
257
258 let file = File::options().create(true).append(true).open(&log_path)?;
259
260 self.current_log = Some(BufWriter::new(file));
261 Ok(())
262 }
263
264 fn close_current_log(&mut self) -> Result<(), Box<dyn std::error::Error>> {
265 if let Some(mut log) = self.current_log.take() {
266 log.flush()?;
267 }
268 Ok(())
269 }
270
271 fn should_rotate_log(&self) -> Result<bool, Box<dyn std::error::Error>> {
272 for entry in fs::read_dir(&self.config.log_dir)? {
274 let entry = entry?;
275 let path = entry.path();
276
277 if path.extension().and_then(|s| s.to_str()) == Some("wal") {
278 let metadata = fs::metadata(&path)?;
279 if metadata.len() >= self.config.max_log_size {
280 return Ok(true);
281 }
282 }
283 }
284 Ok(false)
285 }
286
287 fn rotate_log(&mut self) -> Result<(), Box<dyn std::error::Error>> {
288 self.close_current_log()?;
290
291 if self.config.auto_archive {
293 self.archive_logs()?;
294 }
295
296 self.open_current_log()?;
298
299 Ok(())
300 }
301
302 fn collect_log_files(&self) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
303 let mut log_files = Vec::new();
304
305 if self.config.log_dir.exists() {
307 for entry in fs::read_dir(&self.config.log_dir)? {
308 let entry = entry?;
309 let path = entry.path();
310 if path.extension().and_then(|s| s.to_str()) == Some("wal") {
311 log_files.push(path);
312 }
313 }
314 }
315
316 if self.config.archive_dir.exists() {
318 for entry in fs::read_dir(&self.config.archive_dir)? {
319 let entry = entry?;
320 let path = entry.path();
321 if path.extension().and_then(|s| s.to_str()) == Some("wal") {
322 log_files.push(path);
323 }
324 }
325 }
326
327 Ok(log_files)
328 }
329
330 fn replay_log_file(
331 &self,
332 log_file: &Path,
333 until_timestamp: Option<DateTime<Utc>>,
334 ) -> Result<usize, Box<dyn std::error::Error>> {
335 let file = File::open(log_file)?;
336 let reader = BufReader::new(file);
337 let mut replayed = 0;
338
339 for line in reader.lines() {
340 let line = line?;
341 if line.trim().is_empty() {
342 continue;
343 }
344
345 let entry: TransactionLogEntry = serde_json::from_str(&line)?;
346
347 if let Some(target) = until_timestamp {
349 if entry.timestamp > target {
350 return Ok(replayed);
351 }
352 }
353
354 if !Self::verify_checksum(&entry) {
356 eprintln!(
357 "Warning: Checksum mismatch for transaction {}",
358 entry.transaction_id
359 );
360 continue;
361 }
362
363 replayed += 1;
365 }
366
367 Ok(replayed)
368 }
369
370 fn get_next_transaction_id(log_dir: &Path) -> Result<u64, Box<dyn std::error::Error>> {
371 let mut max_id = 0u64;
372
373 if !log_dir.exists() {
374 return Ok(1);
375 }
376
377 for entry in fs::read_dir(log_dir)? {
379 let entry = entry?;
380 let path = entry.path();
381
382 if path.extension().and_then(|s| s.to_str()) == Some("wal") {
383 let file = File::open(&path)?;
384 let reader = BufReader::new(file);
385
386 for line in reader.lines() {
387 let line = line?;
388 if line.trim().is_empty() {
389 continue;
390 }
391
392 if let Ok(entry) = serde_json::from_str::<TransactionLogEntry>(&line) {
393 max_id = max_id.max(entry.transaction_id);
394 }
395 }
396 }
397 }
398
399 Ok(max_id + 1)
400 }
401
402 fn calculate_checksum(data: &[u8]) -> String {
403 let hash: [u8; 32] = oxicrypto_hash::Sha256.hash_fixed(data);
408 hex::encode(hash)
409 }
410
411 fn verify_checksum(entry: &TransactionLogEntry) -> bool {
412 let calculated = Self::calculate_checksum(&entry.data);
413 calculated == entry.checksum
414 }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct CheckpointMetadata {
420 pub name: String,
421 pub timestamp: DateTime<Utc>,
422 pub last_transaction_id: u64,
423 pub log_files: Vec<PathBuf>,
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use std::env::temp_dir;
430
431 #[test]
432 fn test_transaction_log_creation() {
433 let temp_log_dir = temp_dir().join("oxirs_pitr_test_logs");
434 let temp_archive_dir = temp_dir().join("oxirs_pitr_test_archives");
435
436 let config = PitrConfig {
437 log_dir: temp_log_dir.clone(),
438 archive_dir: temp_archive_dir.clone(),
439 max_log_size: 10_000_000, auto_archive: false,
441 };
442
443 let log = TransactionLog::new(config).unwrap();
444 assert_eq!(log.next_transaction_id, 1);
445
446 let _ = fs::remove_dir_all(temp_log_dir);
448 let _ = fs::remove_dir_all(temp_archive_dir);
449 }
450
451 #[test]
452 fn test_transaction_append() {
453 let temp_log_dir = temp_dir().join("oxirs_pitr_test_logs2");
454 let temp_archive_dir = temp_dir().join("oxirs_pitr_test_archives2");
455
456 let config = PitrConfig {
457 log_dir: temp_log_dir.clone(),
458 archive_dir: temp_archive_dir.clone(),
459 max_log_size: 10_000_000,
460 auto_archive: false,
461 };
462
463 let mut log = TransactionLog::new(config).unwrap();
464
465 let tx1 = log
467 .append(OperationType::Insert, b"test data 1".to_vec())
468 .unwrap();
469 let tx2 = log
470 .append(OperationType::Update, b"test data 2".to_vec())
471 .unwrap();
472 let tx3 = log
473 .append(OperationType::Delete, b"test data 3".to_vec())
474 .unwrap();
475
476 assert_eq!(tx1, 1);
477 assert_eq!(tx2, 2);
478 assert_eq!(tx3, 3);
479
480 let _ = fs::remove_dir_all(temp_log_dir);
482 let _ = fs::remove_dir_all(temp_archive_dir);
483 }
484
485 #[test]
486 fn test_checkpoint_creation() {
487 let temp_log_dir = temp_dir().join("oxirs_pitr_test_logs3");
488 let temp_archive_dir = temp_dir().join("oxirs_pitr_test_archives3");
489
490 let config = PitrConfig {
491 log_dir: temp_log_dir.clone(),
492 archive_dir: temp_archive_dir.clone(),
493 max_log_size: 10_000_000,
494 auto_archive: false,
495 };
496
497 let mut log = TransactionLog::new(config).unwrap();
498
499 log.append(OperationType::Insert, b"data1".to_vec())
501 .unwrap();
502 log.append(OperationType::Insert, b"data2".to_vec())
503 .unwrap();
504
505 let checkpoint_path = log.create_checkpoint("test_checkpoint").unwrap();
507 assert!(checkpoint_path.exists());
508
509 let checkpoints = log.list_checkpoints().unwrap();
511 assert_eq!(checkpoints.len(), 1);
512 assert_eq!(checkpoints[0].name, "test_checkpoint");
513
514 let _ = fs::remove_dir_all(temp_log_dir);
516 let _ = fs::remove_dir_all(temp_archive_dir);
517 }
518}