sql_cli/
search_filter.rs

1use anyhow::Result;
2use fuzzy_matcher::skim::SkimMatcherV2;
3use fuzzy_matcher::FuzzyMatcher;
4use regex::Regex;
5use serde_json::Value;
6
7/// Handles search and filter operations on data
8pub struct SearchFilter;
9
10impl SearchFilter {
11    /// Perform a regex search on data and return matching positions
12    pub fn perform_search(data: &[Vec<String>], pattern: &str) -> Result<Vec<(usize, usize)>> {
13        let mut matches = Vec::new();
14        let regex = Regex::new(pattern)?;
15
16        for (row_idx, row) in data.iter().enumerate() {
17            for (col_idx, cell) in row.iter().enumerate() {
18                if regex.is_match(cell) {
19                    matches.push((row_idx, col_idx));
20                }
21            }
22        }
23
24        Ok(matches)
25    }
26
27    /// Apply a regex filter to JSON data
28    pub fn apply_regex_filter(data: &[Value], pattern: &str) -> Result<Vec<Value>> {
29        let regex = Regex::new(pattern)?;
30        let mut filtered = Vec::new();
31
32        for item in data {
33            if let Some(obj) = item.as_object() {
34                let mut matches = false;
35                for (_key, value) in obj {
36                    let value_str = match value {
37                        Value::String(s) => s.clone(),
38                        Value::Number(n) => n.to_string(),
39                        Value::Bool(b) => b.to_string(),
40                        Value::Null => String::from("null"),
41                        _ => value.to_string(),
42                    };
43
44                    if regex.is_match(&value_str) {
45                        matches = true;
46                        break;
47                    }
48                }
49
50                if matches {
51                    filtered.push(item.clone());
52                }
53            }
54        }
55
56        Ok(filtered)
57    }
58
59    /// Apply fuzzy filter to data and return matching indices
60    #[must_use]
61    pub fn apply_fuzzy_filter(data: &[Value], pattern: &str, score_threshold: i64) -> Vec<usize> {
62        let matcher = SkimMatcherV2::default();
63        let mut filtered_indices = Vec::new();
64
65        for (idx, item) in data.iter().enumerate() {
66            if let Some(obj) = item.as_object() {
67                let mut best_score = 0i64;
68
69                for (_key, value) in obj {
70                    let value_str = match value {
71                        Value::String(s) => s.clone(),
72                        Value::Number(n) => n.to_string(),
73                        Value::Bool(b) => b.to_string(),
74                        Value::Null => String::from("null"),
75                        _ => value.to_string(),
76                    };
77
78                    if let Some(score) = matcher.fuzzy_match(&value_str, pattern) {
79                        best_score = best_score.max(score);
80                    }
81                }
82
83                if best_score > score_threshold {
84                    filtered_indices.push(idx);
85                }
86            }
87        }
88
89        filtered_indices
90    }
91
92    /// Find columns matching a search pattern
93    #[must_use]
94    pub fn find_matching_columns(headers: &[&str], pattern: &str) -> Vec<(usize, String)> {
95        let pattern_lower = pattern.to_lowercase();
96        let mut matching = Vec::new();
97
98        for (idx, &header) in headers.iter().enumerate() {
99            if header.to_lowercase().contains(&pattern_lower) {
100                matching.push((idx, header.to_string()));
101            }
102        }
103
104        matching
105    }
106
107    /// Navigate to next search match
108    #[must_use]
109    pub fn next_match(matches: &[(usize, usize)], current_index: usize) -> Option<(usize, usize)> {
110        if matches.is_empty() {
111            return None;
112        }
113
114        let next_index = (current_index + 1) % matches.len();
115        Some(matches[next_index])
116    }
117
118    /// Navigate to previous search match
119    #[must_use]
120    pub fn previous_match(
121        matches: &[(usize, usize)],
122        current_index: usize,
123    ) -> Option<(usize, usize)> {
124        if matches.is_empty() {
125            return None;
126        }
127
128        let prev_index = if current_index == 0 {
129            matches.len() - 1
130        } else {
131            current_index - 1
132        };
133
134        Some(matches[prev_index])
135    }
136}