1use super::{ReplError, ReplResult};
7use std::collections::VecDeque;
8use std::fs;
9use std::io::Write;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone)]
14pub struct HistoryEntry {
15 pub command: String,
17 pub timestamp: std::time::SystemTime,
19 pub duration: Option<std::time::Duration>,
21 pub success: Option<bool>,
23 pub category: HistoryCategory,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub enum HistoryCategory {
30 Query, Meta, Config, Navigation, System, Unknown,
36}
37
38impl std::fmt::Display for HistoryCategory {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 HistoryCategory::Query => write!(f, "query"),
42 HistoryCategory::Meta => write!(f, "meta"),
43 HistoryCategory::Config => write!(f, "config"),
44 HistoryCategory::Navigation => write!(f, "navigation"),
45 HistoryCategory::System => write!(f, "system"),
46 HistoryCategory::Unknown => write!(f, "unknown"),
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct HistoryFilter {
54 pub pattern: Option<String>,
56 pub category: Option<HistoryCategory>,
58 pub success_only: bool,
60 pub since: Option<std::time::SystemTime>,
62 pub limit: Option<usize>,
64}
65
66impl Default for HistoryFilter {
67 fn default() -> Self {
68 Self {
69 pattern: None,
70 category: None,
71 success_only: false,
72 since: None,
73 limit: Some(50),
74 }
75 }
76}
77
78pub struct HistoryManager {
80 history: VecDeque<HistoryEntry>,
82 max_size: usize,
84 current_position: Option<usize>,
86 history_file: Option<PathBuf>,
88 persistent: bool,
90 stats: HistoryStats,
92}
93
94#[derive(Debug, Default)]
96pub struct HistoryStats {
97 pub total_commands: u64,
98 pub successful_commands: u64,
99 pub failed_commands: u64,
100 pub by_category: std::collections::HashMap<HistoryCategory, u64>,
101 pub avg_duration_ms: f64,
102}
103
104impl HistoryManager {
105 pub fn new(max_size: usize) -> ReplResult<Self> {
107 Ok(Self {
108 history: VecDeque::with_capacity(max_size),
109 max_size,
110 current_position: None,
111 history_file: None,
112 persistent: false,
113 stats: HistoryStats::default(),
114 })
115 }
116
117 pub fn new_persistent(max_size: usize, history_dir: &Path) -> ReplResult<Self> {
119 let history_file = history_dir.join("cqlite_history.txt");
120
121 if let Some(parent) = history_file.parent() {
123 fs::create_dir_all(parent).map_err(ReplError::Io)?;
124 }
125
126 let mut manager = Self {
127 history: VecDeque::with_capacity(max_size),
128 max_size,
129 current_position: None,
130 history_file: Some(history_file),
131 persistent: true,
132 stats: HistoryStats::default(),
133 };
134
135 manager.load_history()?;
137
138 Ok(manager)
139 }
140
141 pub fn add_command(&mut self, command: &str) -> ReplResult<()> {
143 if command.trim().is_empty() {
145 return Ok(());
146 }
147
148 if let Some(last_entry) = self.history.back() {
150 if last_entry.command.trim() == command.trim() {
151 return Ok(());
152 }
153 }
154
155 let entry = HistoryEntry {
156 command: command.to_string(),
157 timestamp: std::time::SystemTime::now(),
158 duration: None,
159 success: None,
160 category: self.categorize_command(command),
161 };
162
163 self.add_entry(entry)?;
164 Ok(())
165 }
166
167 pub fn add_command_with_result(
169 &mut self,
170 command: &str,
171 duration: std::time::Duration,
172 success: bool,
173 ) -> ReplResult<()> {
174 if command.trim().is_empty() {
175 return Ok(());
176 }
177
178 let entry = HistoryEntry {
179 command: command.to_string(),
180 timestamp: std::time::SystemTime::now(),
181 duration: Some(duration),
182 success: Some(success),
183 category: self.categorize_command(command),
184 };
185
186 self.add_entry(entry.clone())?;
187 self.update_stats(&entry);
188
189 Ok(())
190 }
191
192 fn add_entry(&mut self, entry: HistoryEntry) -> ReplResult<()> {
194 while self.history.len() >= self.max_size {
196 self.history.pop_front();
197 }
198
199 self.history.push_back(entry.clone());
200 self.current_position = None;
201
202 if self.persistent {
204 self.persist_entry(&entry)?;
205 }
206
207 Ok(())
208 }
209
210 fn categorize_command(&self, command: &str) -> HistoryCategory {
212 let trimmed = command.trim();
213
214 if trimmed.starts_with(':') || trimmed.starts_with('.') || trimmed.starts_with('\\') {
215 if trimmed.contains("config") || trimmed.contains("set") {
216 HistoryCategory::Config
217 } else if trimmed.contains("use")
218 || trimmed.contains("tables")
219 || trimmed.contains("keyspaces")
220 || trimmed.contains("describe")
221 {
222 HistoryCategory::Navigation
223 } else if trimmed.contains("clear")
224 || trimmed.contains("history")
225 || trimmed.contains("source")
226 {
227 HistoryCategory::System
228 } else {
229 HistoryCategory::Meta
230 }
231 } else {
232 let upper = trimmed.to_uppercase();
233 if upper.starts_with("SELECT")
234 || upper.starts_with("INSERT")
235 || upper.starts_with("UPDATE")
236 || upper.starts_with("DELETE")
237 || upper.starts_with("CREATE")
238 || upper.starts_with("ALTER")
239 || upper.starts_with("DROP")
240 {
241 HistoryCategory::Query
242 } else {
243 HistoryCategory::Unknown
244 }
245 }
246 }
247
248 pub fn recent_commands(&self, limit: usize) -> Vec<String> {
250 self.history
251 .iter()
252 .rev()
253 .take(limit)
254 .map(|entry| entry.command.clone())
255 .collect()
256 }
257
258 pub fn search(&self, filter: &HistoryFilter) -> Vec<&HistoryEntry> {
260 let mut results: Vec<&HistoryEntry> = self
261 .history
262 .iter()
263 .filter(|entry| self.matches_filter(entry, filter))
264 .collect();
265
266 results.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
268
269 if let Some(limit) = filter.limit {
271 results.truncate(limit);
272 }
273
274 results
275 }
276
277 fn matches_filter(&self, entry: &HistoryEntry, filter: &HistoryFilter) -> bool {
279 if let Some(ref pattern) = filter.pattern {
281 if !entry
282 .command
283 .to_lowercase()
284 .contains(&pattern.to_lowercase())
285 {
286 return false;
287 }
288 }
289
290 if let Some(ref category) = filter.category {
292 if entry.category != *category {
293 return false;
294 }
295 }
296
297 if filter.success_only {
299 if let Some(success) = entry.success {
300 if !success {
301 return false;
302 }
303 } else {
304 return false; }
306 }
307
308 if let Some(since) = filter.since {
310 if entry.timestamp < since {
311 return false;
312 }
313 }
314
315 true
316 }
317
318 pub fn previous(&mut self) -> Option<String> {
320 if self.history.is_empty() {
321 return None;
322 }
323
324 let new_position = match self.current_position {
325 None => self.history.len() - 1,
326 Some(pos) => {
327 if pos > 0 {
328 pos - 1
329 } else {
330 return None; }
332 }
333 };
334
335 self.current_position = Some(new_position);
336 Some(self.history[new_position].command.clone())
337 }
338
339 pub fn next(&mut self) -> Option<String> {
341 if let Some(pos) = self.current_position {
342 if pos < self.history.len() - 1 {
343 self.current_position = Some(pos + 1);
344 Some(self.history[pos + 1].command.clone())
345 } else {
346 self.current_position = None;
347 None }
349 } else {
350 None
351 }
352 }
353
354 pub fn reset_position(&mut self) {
356 self.current_position = None;
357 }
358
359 pub fn stats(&self) -> &HistoryStats {
361 &self.stats
362 }
363
364 fn update_stats(&mut self, entry: &HistoryEntry) {
366 self.stats.total_commands += 1;
367
368 if let Some(success) = entry.success {
369 if success {
370 self.stats.successful_commands += 1;
371 } else {
372 self.stats.failed_commands += 1;
373 }
374 }
375
376 *self
378 .stats
379 .by_category
380 .entry(entry.category.clone())
381 .or_insert(0) += 1;
382
383 if let Some(duration) = entry.duration {
385 let duration_ms = duration.as_millis() as f64;
386 let total_duration =
387 self.stats.avg_duration_ms * (self.stats.total_commands - 1) as f64;
388 self.stats.avg_duration_ms =
389 (total_duration + duration_ms) / self.stats.total_commands as f64;
390 }
391 }
392
393 fn load_history(&mut self) -> ReplResult<()> {
395 if let Some(ref path) = self.history_file {
396 if path.exists() {
397 let content = fs::read_to_string(path).map_err(ReplError::Io)?;
398
399 for line in content.lines() {
400 if !line.trim().is_empty() {
401 if let Some(command) = self.parse_history_line(line) {
403 let entry = HistoryEntry {
404 command,
405 timestamp: std::time::SystemTime::now(),
406 duration: None,
407 success: None,
408 category: self.categorize_command(&line),
409 };
410
411 if self.history.len() >= self.max_size {
412 self.history.pop_front();
413 }
414 self.history.push_back(entry);
415 }
416 }
417 }
418 }
419 }
420
421 Ok(())
422 }
423
424 fn parse_history_line(&self, line: &str) -> Option<String> {
426 if line.trim().is_empty() {
430 None
431 } else {
432 Some(line.trim().to_string())
433 }
434 }
435
436 fn persist_entry(&self, entry: &HistoryEntry) -> ReplResult<()> {
438 if let Some(ref path) = self.history_file {
439 let mut file = fs::OpenOptions::new()
440 .create(true)
441 .append(true)
442 .open(path)
443 .map_err(ReplError::Io)?;
444
445 writeln!(file, "{}", entry.command).map_err(ReplError::Io)?;
447 }
448
449 Ok(())
450 }
451
452 pub fn save_history(&self) -> ReplResult<()> {
454 if let Some(ref path) = self.history_file {
455 let mut file = fs::File::create(path).map_err(ReplError::Io)?;
456
457 for entry in &self.history {
458 writeln!(file, "{}", entry.command).map_err(ReplError::Io)?;
459 }
460 }
461
462 Ok(())
463 }
464
465 pub fn clear(&mut self) -> ReplResult<()> {
467 self.history.clear();
468 self.current_position = None;
469 self.stats = HistoryStats::default();
470
471 if let Some(ref path) = self.history_file {
473 if path.exists() {
474 fs::remove_file(path).map_err(ReplError::Io)?;
475 }
476 }
477
478 Ok(())
479 }
480
481 pub fn len(&self) -> usize {
483 self.history.len()
484 }
485
486 pub fn is_empty(&self) -> bool {
488 self.history.is_empty()
489 }
490
491 pub fn export_text(&self) -> String {
493 let mut output = String::new();
494
495 output.push_str(&format!(
496 "# CQLite Command History ({})\n",
497 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S")
498 ));
499 output.push_str(&format!("# Total commands: {}\n", self.history.len()));
500 output.push_str("# Format: command\n\n");
501
502 for entry in &self.history {
503 output.push_str(&entry.command);
504 output.push('\n');
505 }
506
507 output
508 }
509
510 pub fn export_detailed(&self) -> String {
512 let mut output = String::new();
513
514 output.push_str(&format!(
515 "# CQLite Detailed Command History ({})\n",
516 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S")
517 ));
518 output.push_str(&format!(
519 "# Total commands: {}\n",
520 self.stats.total_commands
521 ));
522 output.push_str(&format!(
523 "# Successful: {}\n",
524 self.stats.successful_commands
525 ));
526 output.push_str(&format!("# Failed: {}\n", self.stats.failed_commands));
527 output.push_str(&format!(
528 "# Average duration: {:.2}ms\n",
529 self.stats.avg_duration_ms
530 ));
531 output.push_str("# Format: timestamp | category | duration | success | command\n\n");
532
533 for entry in &self.history {
534 let timestamp = entry
535 .timestamp
536 .duration_since(std::time::UNIX_EPOCH)
537 .unwrap_or_default()
538 .as_secs();
539
540 let duration_str = entry
541 .duration
542 .map(|d| format!("{:.2}ms", d.as_millis()))
543 .unwrap_or_else(|| "N/A".to_string());
544
545 let success_str = entry
546 .success
547 .map(|s| if s { "OK" } else { "ERR" })
548 .unwrap_or("N/A");
549
550 output.push_str(&format!(
551 "{} | {} | {} | {} | {}\n",
552 timestamp, entry.category, duration_str, success_str, entry.command
553 ));
554 }
555
556 output
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 use tempfile::tempdir;
564
565 #[test]
566 fn test_basic_history() {
567 let mut history = HistoryManager::new(10).unwrap();
568
569 history.add_command("SELECT * FROM users").unwrap();
570 history.add_command("SELECT count(*) FROM orders").unwrap();
571
572 assert_eq!(history.len(), 2);
573
574 let recent = history.recent_commands(5);
575 assert_eq!(recent.len(), 2);
576 assert_eq!(recent[0], "SELECT count(*) FROM orders");
577 assert_eq!(recent[1], "SELECT * FROM users");
578 }
579
580 #[test]
581 fn test_navigation() {
582 let mut history = HistoryManager::new(10).unwrap();
583
584 history.add_command("command1").unwrap();
585 history.add_command("command2").unwrap();
586 history.add_command("command3").unwrap();
587
588 assert_eq!(history.previous(), Some("command3".to_string()));
590 assert_eq!(history.previous(), Some("command2".to_string()));
591 assert_eq!(history.previous(), Some("command1".to_string()));
592 assert_eq!(history.previous(), None); assert_eq!(history.next(), Some("command2".to_string()));
596 assert_eq!(history.next(), Some("command3".to_string()));
597 assert_eq!(history.next(), None); }
599
600 #[test]
601 fn test_search_filter() {
602 let mut history = HistoryManager::new(10).unwrap();
603
604 history.add_command("SELECT * FROM users").unwrap();
605 history.add_command(":tables").unwrap();
606 history.add_command("SELECT * FROM orders").unwrap();
607
608 let filter = HistoryFilter {
609 pattern: Some("SELECT".to_string()),
610 ..Default::default()
611 };
612
613 let results = history.search(&filter);
614 assert_eq!(results.len(), 2);
615 assert!(results.iter().all(|r| r.command.contains("SELECT")));
616 }
617
618 #[test]
619 fn test_categorization() {
620 let history = HistoryManager::new(10).unwrap();
621
622 assert_eq!(
623 history.categorize_command("SELECT * FROM users"),
624 HistoryCategory::Query
625 );
626 assert_eq!(history.categorize_command(":help"), HistoryCategory::Meta);
627 assert_eq!(
628 history.categorize_command(":config show"),
629 HistoryCategory::Config
630 );
631 assert_eq!(
632 history.categorize_command(":tables"),
633 HistoryCategory::Navigation
634 );
635 assert_eq!(
636 history.categorize_command(":clear"),
637 HistoryCategory::System
638 );
639 }
640
641 #[test]
642 fn test_persistent_history() {
643 let temp_dir = tempdir().unwrap();
644 let temp_path = temp_dir.path();
645
646 {
647 let mut history = HistoryManager::new_persistent(10, temp_path).unwrap();
648 history.add_command("SELECT 1").unwrap();
649 history.add_command("SELECT 2").unwrap();
650 }
651
652 let history = HistoryManager::new_persistent(10, temp_path).unwrap();
654 assert_eq!(history.len(), 2);
655
656 let recent = history.recent_commands(5);
657 assert!(recent.contains(&"SELECT 1".to_string()));
658 assert!(recent.contains(&"SELECT 2".to_string()));
659 }
660}