1use crate::app_paths::AppPaths;
2use crate::history_protection::HistoryProtection;
3use anyhow::Result;
4use chrono::{DateTime, Utc};
5use fuzzy_matcher::skim::SkimMatcherV2;
6use fuzzy_matcher::FuzzyMatcher;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct QueryMetadata {
14 #[serde(default)]
15 pub tables: Vec<String>, #[serde(default)]
17 pub select_columns: Vec<String>, #[serde(default)]
19 pub where_columns: Vec<String>, #[serde(default)]
21 pub order_by_columns: Vec<String>, #[serde(default)]
23 pub functions_used: Vec<String>, #[serde(default)]
25 pub query_type: String, }
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct HistoryEntry {
30 pub command: String,
31 pub timestamp: DateTime<Utc>,
32 pub execution_count: u32,
33 pub success: bool,
34 pub duration_ms: Option<u64>,
35 #[serde(default)]
36 pub schema_columns: Vec<String>, #[serde(default)]
38 pub data_source: Option<String>, #[serde(default)]
40 pub metadata: Option<QueryMetadata>, #[serde(default)]
42 pub is_starred: bool, #[serde(default)]
44 pub session_id: Option<String>, }
46
47#[derive(Debug, Clone)]
48pub struct HistoryMatch {
49 pub entry: HistoryEntry,
50 pub score: i64,
51 pub indices: Vec<usize>,
52}
53
54pub struct CommandHistory {
55 entries: Vec<HistoryEntry>,
56 history_file: PathBuf,
57 matcher: SkimMatcherV2,
58 command_counts: HashMap<String, u32>,
59 session_id: String,
60 session_entries: Vec<HistoryEntry>, protection: HistoryProtection,
62}
63
64impl CommandHistory {
65 pub fn new() -> Result<Self> {
66 let history_file = AppPaths::history_file()
67 .map_err(|e| anyhow::anyhow!("Failed to get history file path: {}", e))?;
68 Self::with_history_file(history_file)
69 }
70
71 pub fn with_history_file(history_file: PathBuf) -> Result<Self> {
77 let backup_dir = history_file
79 .parent()
80 .unwrap_or(std::path::Path::new("."))
81 .join("history_backups");
82
83 let session_id = format!("session_{}", Utc::now().timestamp_millis());
85
86 let mut history = Self {
87 entries: Vec::new(),
88 history_file,
89 matcher: SkimMatcherV2::default(),
90 command_counts: HashMap::new(),
91 session_id,
92 session_entries: Vec::new(),
93 protection: HistoryProtection::new(backup_dir),
94 };
95
96 history.load_from_file()?;
97
98 if !history.entries.is_empty() {
101 history.clean_and_save()?;
102 }
103
104 Ok(history)
105 }
106
107 pub fn add_entry(
108 &mut self,
109 command: String,
110 success: bool,
111 duration_ms: Option<u64>,
112 ) -> Result<()> {
113 self.add_entry_with_schema(command, success, duration_ms, Vec::new(), None)
114 }
115
116 pub fn add_entry_with_schema(
117 &mut self,
118 command: String,
119 success: bool,
120 duration_ms: Option<u64>,
121 schema_columns: Vec<String>,
122 data_source: Option<String>,
123 ) -> Result<()> {
124 if command.trim().is_empty() {
126 return Ok(());
127 }
128
129 if let Some(last_entry) = self.entries.last() {
131 if last_entry.command == command {
132 return Ok(());
133 }
134 }
135
136 let metadata = self.extract_query_metadata(&command);
138
139 let entry = HistoryEntry {
140 command: command.clone(),
141 timestamp: Utc::now(),
142 execution_count: *self.command_counts.get(&command).unwrap_or(&0) + 1,
143 success,
144 duration_ms,
145 schema_columns,
146 data_source,
147 metadata,
148 is_starred: false,
149 session_id: Some(self.session_id.clone()),
150 };
151
152 self.session_entries.push(entry.clone());
154
155 *self.command_counts.entry(command.clone()).or_insert(0) += 1;
157
158 self.entries.retain(|e| e.command != command);
161
162 self.entries.push(entry);
163
164 if self.entries.len() > 1000 {
166 self.entries.drain(0..self.entries.len() - 1000);
167 }
168
169 self.save_to_file()?;
170 Ok(())
171 }
172
173 pub fn search(&self, query: &str) -> Vec<HistoryMatch> {
174 self.search_with_schema(query, &[], None)
175 }
176
177 pub fn search_with_schema(
178 &self,
179 query: &str,
180 current_columns: &[String],
181 current_source: Option<&str>,
182 ) -> Vec<HistoryMatch> {
183 if query.is_empty() {
184 let mut entries: Vec<_> = self
186 .entries
187 .iter()
188 .rev()
189 .take(100)
190 .map(|entry| {
191 let schema_score =
192 self.calculate_schema_match_score(entry, current_columns, current_source);
193 HistoryMatch {
194 entry: entry.clone(),
195 score: 100 + schema_score,
196 indices: Vec::new(),
197 }
198 })
199 .collect();
200
201 entries.sort_by(|a, b| b.score.cmp(&a.score));
202 entries.truncate(50);
203 return entries;
204 }
205
206 let mut matches: Vec<HistoryMatch> = self
207 .entries
208 .iter()
209 .filter_map(|entry| {
210 if let Some((score, indices)) = self.matcher.fuzzy_indices(&entry.command, query) {
211 let schema_score =
212 self.calculate_schema_match_score(entry, current_columns, current_source);
213 Some(HistoryMatch {
214 entry: entry.clone(),
215 score: score + schema_score,
216 indices,
217 })
218 } else {
219 None
220 }
221 })
222 .collect();
223
224 matches.sort_by(|a, b| {
226 let score_cmp = b.score.cmp(&a.score);
228 if score_cmp != std::cmp::Ordering::Equal {
229 return score_cmp;
230 }
231
232 let count_cmp = b.entry.execution_count.cmp(&a.entry.execution_count);
234 if count_cmp != std::cmp::Ordering::Equal {
235 return count_cmp;
236 }
237
238 b.entry.timestamp.cmp(&a.entry.timestamp)
240 });
241
242 matches.truncate(20); matches
244 }
245
246 fn calculate_schema_match_score(
247 &self,
248 entry: &HistoryEntry,
249 current_columns: &[String],
250 current_source: Option<&str>,
251 ) -> i64 {
252 let mut score = 0i64;
253
254 if let (Some(entry_source), Some(current)) = (&entry.data_source, current_source) {
256 if entry_source == current {
257 score += 50; }
259 }
260
261 if !current_columns.is_empty() && !entry.schema_columns.is_empty() {
263 let matching_columns = entry
264 .schema_columns
265 .iter()
266 .filter(|col| current_columns.contains(col))
267 .count();
268
269 let total_columns = entry.schema_columns.len().max(current_columns.len());
270 if total_columns > 0 {
271 let match_percentage = (matching_columns * 100) / total_columns;
273 score += (match_percentage as i64) / 2; }
275 }
276
277 if let Some(metadata) = &entry.metadata {
279 let metadata_columns: Vec<&String> = metadata
280 .select_columns
281 .iter()
282 .chain(metadata.where_columns.iter())
283 .chain(metadata.order_by_columns.iter())
284 .collect();
285
286 let matching_metadata = metadata_columns
287 .iter()
288 .filter(|col| current_columns.contains(col))
289 .count();
290
291 if matching_metadata > 0 {
292 score += (matching_metadata as i64) * 5; }
294 }
295
296 score
297 }
298
299 fn extract_query_metadata(&self, query: &str) -> Option<QueryMetadata> {
300 let query_upper = query.to_uppercase();
301
302 let query_type = if query_upper.starts_with("SELECT") {
304 "SELECT"
305 } else if query_upper.starts_with("INSERT") {
306 "INSERT"
307 } else if query_upper.starts_with("UPDATE") {
308 "UPDATE"
309 } else if query_upper.starts_with("DELETE") {
310 "DELETE"
311 } else {
312 "OTHER"
313 }
314 .to_string();
315
316 let mut tables = Vec::new();
318 if let Some(from_idx) = query_upper.find(" FROM ") {
319 let after_from = &query[from_idx + 6..];
320 if let Some(end_idx) = after_from.find([' ', '(', ';']) {
321 let table_name = after_from[..end_idx].trim().to_string();
322 if !table_name.is_empty() {
323 tables.push(table_name);
324 }
325 }
326 }
327
328 let mut select_columns = Vec::new();
330 if query_type == "SELECT" {
331 if let Some(select_idx) = query_upper.find("SELECT ") {
332 let after_select = &query[select_idx + 7..];
333 if let Some(from_idx) = after_select.to_uppercase().find(" FROM") {
334 let select_clause = &after_select[..from_idx];
335 if !select_clause.trim().eq("*") {
336 for col in select_clause.split(',') {
338 let col_name = col
339 .split_whitespace()
340 .next()
341 .unwrap_or("")
342 .trim_matches('"')
343 .to_string();
344 if !col_name.is_empty() {
345 select_columns.push(col_name);
346 }
347 }
348 }
349 }
350 }
351 }
352
353 let mut where_columns = Vec::new();
355 let mut functions_used = Vec::new();
356 if let Some(where_idx) = query_upper.find(" WHERE ") {
357 let after_where = &query[where_idx + 7..];
358
359 let linq_methods = [
361 "Contains",
362 "StartsWith",
363 "EndsWith",
364 "Length",
365 "ToUpper",
366 "ToLower",
367 "IsNullOrEmpty",
368 ];
369 for method in &linq_methods {
370 if after_where.contains(method) {
371 functions_used.push((*method).to_string());
372 }
373 }
374
375 let words: Vec<&str> = after_where
378 .split(|c: char| !c.is_alphanumeric() && c != '_')
379 .filter(|s| !s.is_empty())
380 .collect();
381
382 for (i, word) in words.iter().enumerate() {
383 if i + 1 < words.len() {
385 let next = words[i + 1];
386 if linq_methods.contains(&next)
387 || ["IS", "NOT", "LIKE", "BETWEEN"].contains(&next.to_uppercase().as_str())
388 {
389 where_columns.push((*word).to_string());
390 }
391 }
392 }
393 }
394
395 let mut order_by_columns = Vec::new();
397 if let Some(order_idx) = query_upper.find(" ORDER BY ") {
398 let after_order = &query[order_idx + 10..];
399 let end_idx = after_order.find([';', ')']).unwrap_or(after_order.len());
400 let order_clause = &after_order[..end_idx];
401
402 for col in order_clause.split(',') {
403 let col_name = col
404 .split_whitespace()
405 .next()
406 .unwrap_or("")
407 .trim_matches('"')
408 .to_string();
409 if !col_name.is_empty()
410 && col_name.to_uppercase() != "ASC"
411 && col_name.to_uppercase() != "DESC"
412 {
413 order_by_columns.push(col_name);
414 }
415 }
416 }
417
418 Some(QueryMetadata {
419 tables,
420 select_columns,
421 where_columns,
422 order_by_columns,
423 functions_used,
424 query_type,
425 })
426 }
427
428 pub fn get_recent(&self, limit: usize) -> Vec<&HistoryEntry> {
429 self.entries.iter().rev().take(limit).collect()
430 }
431
432 pub fn get_all(&self) -> &[HistoryEntry] {
433 &self.entries
434 }
435
436 pub fn get_last_entry(&self) -> Option<&HistoryEntry> {
437 self.entries.last()
438 }
439
440 pub fn get_session_entries(&self) -> &[HistoryEntry] {
442 &self.session_entries
443 }
444
445 pub fn get_navigation_entries(&self) -> Vec<HistoryEntry> {
447 let mut entries = self.session_entries.clone();
448
449 for entry in &self.entries {
451 if entry.is_starred
452 && !self
453 .session_entries
454 .iter()
455 .any(|e| e.command == entry.command)
456 {
457 entries.push(entry.clone());
458 }
459 }
460
461 entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
463
464 let mut seen = std::collections::HashSet::new();
466 entries.retain(|e| seen.insert(e.command.clone()));
467
468 entries
469 }
470
471 pub fn toggle_star(&mut self, command: &str) -> Result<()> {
473 for entry in &mut self.entries {
475 if entry.command == command {
476 entry.is_starred = !entry.is_starred;
477 break;
478 }
479 }
480
481 for entry in &mut self.session_entries {
483 if entry.command == command {
484 entry.is_starred = !entry.is_starred;
485 break;
486 }
487 }
488
489 self.save_to_file()
490 }
491
492 pub fn clear(&mut self) -> Result<()> {
493 let current_count = self.entries.len();
495 if current_count > 0 {
496 eprintln!("[HISTORY WARNING] Clearing {current_count} entries - creating backup");
497 if let Ok(content) = serde_json::to_string_pretty(&self.entries) {
498 self.protection.backup_before_write(&content, current_count);
499 }
500 }
501 self.entries.clear();
502 self.command_counts.clear();
503 self.save_to_file()?;
504 Ok(())
505 }
506
507 fn load_from_file(&mut self) -> Result<()> {
508 if !self.history_file.exists() {
509 eprintln!("[History] No history file found at {:?}", self.history_file);
510 return Ok(());
511 }
512
513 eprintln!("[History] Loading history from {:?}", self.history_file);
514 let content = fs::read_to_string(&self.history_file)?;
515 if content.trim().is_empty() {
516 eprintln!("[History] History file is empty");
517 return Ok(());
518 }
519
520 let entries: Vec<HistoryEntry> = match serde_json::from_str(&content) {
522 Ok(entries) => entries,
523 Err(e) => {
524 eprintln!("[History] ERROR: Failed to parse history file: {e}");
525 eprintln!("[History] Attempting recovery from backup...");
526
527 if let Some(backup_content) = self.protection.recover_from_backup() {
529 eprintln!("[History] Found backup, attempting to restore...");
530
531 match serde_json::from_str::<Vec<HistoryEntry>>(&backup_content) {
533 Ok(backup_entries) => {
534 eprintln!(
535 "[History] Successfully recovered {} entries from backup",
536 backup_entries.len()
537 );
538
539 fs::write(&self.history_file, &backup_content)?;
541
542 let corrupted_path = self.history_file.with_extension("json.corrupted");
544 let _ = fs::rename(
545 self.history_file.with_extension("json"),
546 &corrupted_path,
547 );
548 eprintln!("[History] Corrupted file moved to {corrupted_path:?}");
549
550 backup_entries
551 }
552 Err(backup_err) => {
553 eprintln!("[History] Backup also corrupted: {backup_err}");
554 eprintln!("[History] Starting with empty history");
555 Vec::new()
556 }
557 }
558 } else {
559 eprintln!("[History] No backup available, starting with empty history");
560
561 let corrupted_path = self.history_file.with_extension("json.corrupted");
563 let _ = fs::copy(&self.history_file, &corrupted_path);
564 eprintln!("[History] Corrupted file copied to {corrupted_path:?}");
565
566 Vec::new()
567 }
568 }
569 };
570 eprintln!(
571 "[History] Loaded {} entries from history file",
572 entries.len()
573 );
574 let original_count = entries.len();
575
576 let mut seen_commands = std::collections::HashSet::new();
579 let mut deduplicated = Vec::new();
580
581 for entry in entries.into_iter().rev() {
583 if seen_commands.insert(entry.command.clone()) {
584 deduplicated.push(entry);
585 }
586 }
587
588 deduplicated.reverse();
590
591 deduplicated.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
593
594 let removed_count = original_count - deduplicated.len();
596 if removed_count > 0 {
597 eprintln!("[sql-cli] Cleaned {removed_count} duplicate commands from history");
598 }
599
600 self.command_counts.clear();
602 for entry in &deduplicated {
603 *self
604 .command_counts
605 .entry(entry.command.clone())
606 .or_insert(0) = entry.execution_count;
607 }
608
609 self.entries = deduplicated;
610 eprintln!(
611 "[History] Final history contains {} unique entries",
612 self.entries.len()
613 );
614 Ok(())
615 }
616
617 fn save_to_file(&self) -> Result<()> {
618 let new_content = serde_json::to_string_pretty(&self.entries)?;
620 let new_count = self.entries.len();
621
622 let old_count = if self.history_file.exists() {
624 if let Ok(existing) = fs::read_to_string(&self.history_file) {
625 existing.matches("\"command\":").count()
626 } else {
627 0
628 }
629 } else {
630 0
631 };
632
633 if !self
635 .protection
636 .validate_write(old_count, new_count, &new_content)
637 {
638 eprintln!("[HISTORY PROTECTION] Write blocked! Attempting recovery from backup...");
639 if let Some(backup_content) = self.protection.recover_from_backup() {
640 fs::write(&self.history_file, backup_content)?;
641 return Ok(());
642 }
643 return Err(anyhow::anyhow!("History write validation failed"));
644 }
645
646 if old_count > 0 && (old_count != new_count || old_count > 10) {
648 self.protection.backup_before_write(&new_content, new_count);
649 }
650
651 let temp_file = self.history_file.with_extension("json.tmp");
654 fs::write(&temp_file, new_content)?;
655
656 fs::rename(temp_file, &self.history_file)?;
658 Ok(())
659 }
660
661 pub fn clean_and_save(&mut self) -> Result<()> {
664 self.save_to_file()?;
667 Ok(())
668 }
669
670 pub fn stats(&self) -> HistoryStats {
671 let total_commands = self.entries.len();
672 let unique_commands = self.command_counts.len();
673 let successful_commands = self.entries.iter().filter(|e| e.success).count();
674 let failed_commands = total_commands - successful_commands;
675
676 let most_used = self
677 .command_counts
678 .iter()
679 .max_by_key(|(_, &count)| count)
680 .map(|(cmd, &count)| (cmd.clone(), count));
681
682 HistoryStats {
683 total_commands,
684 unique_commands,
685 successful_commands,
686 failed_commands,
687 most_used_command: most_used,
688 }
689 }
690}
691
692#[derive(Debug)]
693pub struct HistoryStats {
694 pub total_commands: usize,
695 pub unique_commands: usize,
696 pub successful_commands: usize,
697 pub failed_commands: usize,
698 pub most_used_command: Option<(String, u32)>,
699}
700
701impl Clone for CommandHistory {
702 fn clone(&self) -> Self {
703 Self {
704 entries: self.entries.clone(),
705 history_file: self.history_file.clone(),
706 matcher: SkimMatcherV2::default(), command_counts: self.command_counts.clone(),
708 session_id: self.session_id.clone(),
709 session_entries: self.session_entries.clone(),
710 protection: HistoryProtection::new(
711 self.history_file
712 .parent()
713 .unwrap_or(std::path::Path::new("."))
714 .join("history_backups"),
715 ),
716 }
717 }
718}
719
720impl Default for CommandHistory {
721 fn default() -> Self {
722 let session_id = format!("session_{}", Utc::now().timestamp_millis());
723 Self::new().unwrap_or_else(|_| {
724 let history_file =
725 AppPaths::history_file().unwrap_or_else(|_| PathBuf::from(".sql_cli_history.json"));
726 let backup_dir = history_file
727 .parent()
728 .unwrap_or(std::path::Path::new("."))
729 .join("history_backups");
730 Self {
731 entries: Vec::new(),
732 history_file,
733 matcher: SkimMatcherV2::default(),
734 command_counts: HashMap::new(),
735 session_id,
736 session_entries: Vec::new(),
737 protection: HistoryProtection::new(backup_dir),
738 }
739 })
740 }
741}