const PATTERNS: &[&str] = &[
"SHOW TABLES",
"SHOW DATABASES",
"SHOW FUNCTIONS",
"SHOW USERS",
"SHOW ROLES",
"SHOW ENGINES",
"SHOW STAGES",
"SHOW METRICS",
"SHOW WAREHOUSES",
"SHOW PROCESSLIST",
"SHOW VARIABLES",
"SHOW SETTINGS",
"SHOW LOCKS",
"SHOW COLUMNS",
"SHOW CATALOGS",
"SHOW USER FUNCTIONS",
"SHOW TABLE FUNCTIONS",
"SHOW INDEXES",
"SHOW STATISTICS",
"SHOW WORKLOAD GROUPS",
"SHOW ONLINE NODES",
"VACUUM DROP TABLE",
"VACUUM TEMPORARY FILES",
"VACUUM TEMPORARY TABLES",
"VACUUM VIRTUAL COLUMN",
];
pub fn suggest_correction(input: &str) -> Option<String> {
let input = input.trim();
if input.len() < 2 || input.len() > 128 {
return None;
}
if let Some(help) = context_help(input) {
return Some(help);
}
error_correction(input)
}
fn context_help(input: &str) -> Option<String> {
let words: Vec<&str> = input.split_whitespace().collect();
if words.len() != 1 {
return None;
}
let prefix = words[0].to_uppercase();
let matches: Vec<&str> = PATTERNS
.iter()
.filter(|p| p.split_whitespace().next().unwrap() == prefix)
.take(3)
.copied()
.collect();
match matches.len() {
2 => Some(format!("Try: `{}` or `{}`", matches[0], matches[1])),
3 => Some(format!(
"Try: `{}`, `{}`, or `{}`",
matches[0], matches[1], matches[2]
)),
_ => None,
}
}
fn error_correction(input: &str) -> Option<String> {
let input_tokens: Vec<&str> = input.split_whitespace().collect();
let input_upper = input.trim().to_uppercase();
if PATTERNS.iter().any(|&p| p == input_upper) {
return None;
}
if input_tokens.is_empty() {
return None;
}
let first_token = input_tokens[0].to_uppercase();
let mut valid_starts: Vec<&str> = PATTERNS
.iter()
.map(|pattern| pattern.split_whitespace().next().unwrap())
.collect();
valid_starts.sort();
valid_starts.dedup();
let is_sql_command = valid_starts.contains(&first_token.as_str())
|| valid_starts.iter().any(|&keyword| {
let distance = edit_distance(&first_token, keyword);
distance <= 2 && distance < keyword.len() / 2
});
if !is_sql_command {
return None;
}
let mut candidates: Vec<(&str, f64)> = Vec::new();
for &pattern in PATTERNS {
let score = calculate_similarity(&input_tokens, pattern);
if score > 0.0 {
candidates.push((pattern, score));
}
}
if candidates.is_empty() {
return None;
}
candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let best_score = candidates[0].1;
if best_score < 4.0 {
return None;
}
let threshold = best_score - 1.0;
let good_candidates: Vec<&str> = candidates
.iter()
.take_while(|(_, score)| *score >= threshold)
.take(3)
.map(|(pattern, _)| *pattern)
.collect();
match good_candidates.len() {
1 => Some(format!("Did you mean `{}`?", good_candidates[0])),
2 => Some(format!(
"Did you mean `{}` or `{}`?",
good_candidates[0], good_candidates[1]
)),
_ => Some(format!(
"Did you mean `{}`, `{}`, or `{}`?",
good_candidates[0], good_candidates[1], good_candidates[2]
)),
}
}
fn calculate_similarity(input_tokens: &[&str], pattern: &str) -> f64 {
let pattern_tokens: Vec<&str> = pattern.split_whitespace().collect();
let mut total_score = 0.0;
let mut matched_tokens = 0;
for (i, &input_token) in input_tokens.iter().enumerate() {
if i >= pattern_tokens.len() {
break;
}
let pattern_token = pattern_tokens[i];
if input_token.eq_ignore_ascii_case(pattern_token) {
total_score += 3.0;
matched_tokens += 1;
} else if pattern_token
.to_lowercase()
.starts_with(&input_token.to_lowercase())
{
let input_len = input_token.len() as f64;
let pattern_len = pattern_token.len() as f64;
let prefix_ratio = input_len / pattern_len;
if input_len >= 3.0 || prefix_ratio > 0.5 {
total_score += 1.5 + prefix_ratio * 1.0; } else {
total_score += 0.5; }
matched_tokens += 1;
} else {
let distance = edit_distance(input_token, pattern_token);
let max_distance = if pattern_token.len() > 6 { 3 } else { 2 };
if distance <= max_distance && distance < pattern_token.len() / 2 {
total_score += 2.0 - (distance as f64 * 0.5);
matched_tokens += 1;
}
}
}
if matched_tokens == input_tokens.len() && matched_tokens > 0 {
total_score += matched_tokens as f64;
}
total_score
}
fn edit_distance(a: &str, b: &str) -> usize {
let a = a.to_lowercase();
let b = b.to_lowercase();
if a.len().abs_diff(b.len()) > 2 {
return 3;
}
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut curr = vec![0; b.len() + 1];
for (i, ch_a) in a.chars().enumerate() {
curr[0] = i + 1;
for (j, ch_b) in b.chars().enumerate() {
curr[j + 1] = if ch_a == ch_b {
prev[j]
} else {
1 + prev[j].min(prev[j + 1]).min(curr[j])
};
}
std::mem::swap(&mut prev, &mut curr);
}
prev[b.len()]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_typo_corrections() {
assert_eq!(
suggest_correction("show tabl"), Some("Did you mean `SHOW TABLE FUNCTIONS` or `SHOW TABLES`?".to_string())
);
assert_eq!(
suggest_correction("vacum drop table"), Some("Did you mean `VACUUM DROP TABLE`?".to_string())
);
assert_eq!(
suggest_correction("vacuum tempare files"),
Some("Did you mean `VACUUM TEMPORARY FILES`?".to_string())
);
}
#[test]
fn test_multiple_suggestions() {
assert_eq!(
suggest_correction("show table"),
Some("Did you mean `SHOW TABLE FUNCTIONS` or `SHOW TABLES`?".to_string())
);
assert_eq!(
suggest_correction("vacuum temp"),
Some("Did you mean `VACUUM TEMPORARY FILES` or `VACUUM TEMPORARY TABLES`?".to_string())
);
}
#[test]
fn test_context_help() {
assert_eq!(
suggest_correction("vacuum"),
Some(
"Try: `VACUUM DROP TABLE`, `VACUUM TEMPORARY FILES`, or `VACUUM TEMPORARY TABLES`"
.to_string()
)
);
let result = suggest_correction("show").unwrap();
assert!(result.starts_with("Try: "));
assert!(result.contains("SHOW TABLES"));
}
#[test]
fn test_exact_matches() {
assert_eq!(suggest_correction("show tables"), None);
assert_eq!(suggest_correction("VACUUM DROP TABLE"), None);
assert_eq!(suggest_correction("Show Tables"), None);
}
#[test]
fn test_no_suggestions() {
assert_eq!(suggest_correction("xyz abc def"), None);
assert_eq!(suggest_correction("create index"), None);
assert_eq!(suggest_correction("s"), None);
assert_eq!(suggest_correction(""), None);
}
#[test]
fn test_similarity_scoring() {
assert!(
calculate_similarity(&["show", "tables"], "SHOW TABLES")
> calculate_similarity(&["show", "table"], "SHOW TABLES")
);
}
#[test]
fn test_edit_distance() {
assert_eq!(edit_distance("show", "show"), 0);
assert_eq!(edit_distance("tempare", "temporary"), 3);
assert_eq!(edit_distance("tabl", "tables"), 2); }
#[test]
fn test_performance_limits() {
assert_eq!(suggest_correction("a"), None);
assert_eq!(suggest_correction(""), None);
let long_sql = "a".repeat(129);
assert_eq!(suggest_correction(&long_sql), None);
assert!(suggest_correction("show table").is_some());
}
#[test]
fn test_prefix_matching_accuracy() {
let result = suggest_correction("show tab").unwrap();
assert_eq!(
result,
"Did you mean `SHOW TABLE FUNCTIONS` or `SHOW TABLES`?".to_string()
);
}
#[test]
fn test_valid_starts_extraction() {
let mut expected_starts: Vec<&str> = PATTERNS
.iter()
.map(|pattern| pattern.split_whitespace().next().unwrap())
.collect();
expected_starts.sort();
expected_starts.dedup();
assert!(expected_starts.contains(&"SHOW"));
assert!(expected_starts.contains(&"VACUUM"));
assert_eq!(
suggest_correction("show table"),
Some("Did you mean `SHOW TABLE FUNCTIONS` or `SHOW TABLES`?".to_string())
);
assert_eq!(
suggest_correction("vacuum temp"),
Some("Did you mean `VACUUM TEMPORARY FILES` or `VACUUM TEMPORARY TABLES`?".to_string())
);
assert_eq!(suggest_correction("create unknown"), None);
assert_eq!(suggest_correction("select unknown"), None);
}
}