sql_cli/data/
csv_fixes.rs

1// Helper functions for CSV handling improvements
2
3use std::collections::HashMap;
4
5/// Check if a column name needs quoting (contains spaces or special characters)
6#[must_use]
7pub fn needs_quoting(column_name: &str) -> bool {
8    column_name.contains(' ')
9        || column_name.contains('-')
10        || column_name.contains('.')
11        || column_name.contains('(')
12        || column_name.contains(')')
13        || column_name.contains('[')
14        || column_name.contains(']')
15        || column_name.contains('"')
16        || column_name.contains('\'')
17}
18
19/// Quote a column name if necessary
20#[must_use]
21pub fn quote_if_needed(column_name: &str) -> String {
22    if needs_quoting(column_name) {
23        format!("\"{}\"", column_name.replace('"', "\"\""))
24    } else {
25        column_name.to_string()
26    }
27}
28
29/// Build a case-insensitive lookup map for column names
30/// Maps lowercase column names to their original case versions
31#[must_use]
32pub fn build_column_lookup(headers: &[String]) -> HashMap<String, String> {
33    let mut lookup = HashMap::new();
34    for header in headers {
35        lookup.insert(header.to_lowercase(), header.clone());
36    }
37    lookup
38}
39
40/// Find a column by name (case-insensitive)
41#[must_use]
42pub fn find_column_case_insensitive<'a>(
43    obj: &'a serde_json::Map<String, serde_json::Value>,
44    column_name: &str,
45    lookup: &HashMap<String, String>,
46) -> Option<(&'a String, &'a serde_json::Value)> {
47    // First try exact match
48    if let Some(value) = obj.get_key_value(column_name) {
49        return Some(value);
50    }
51
52    // Then try case-insensitive match using lookup
53    if let Some(actual_name) = lookup.get(&column_name.to_lowercase()) {
54        obj.get_key_value(actual_name)
55    } else {
56        // Fallback: linear search (for quoted columns)
57        let column_unquoted = column_name.trim_matches('"');
58        for (key, value) in obj {
59            if key == column_unquoted || key.to_lowercase() == column_unquoted.to_lowercase() {
60                return Some((key, value));
61            }
62        }
63        None
64    }
65}
66
67/// Parse a column name that might be quoted
68#[must_use]
69pub fn parse_column_name(column: &str) -> &str {
70    column.trim().trim_matches('"').trim_matches('\'')
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_needs_quoting() {
79        assert!(!needs_quoting("City"));
80        assert!(!needs_quoting("customer_id"));
81        assert!(needs_quoting("Phone 1"));
82        assert!(needs_quoting("Customer-ID"));
83        assert!(needs_quoting("Price ($)"));
84    }
85
86    #[test]
87    fn test_quote_if_needed() {
88        assert_eq!(quote_if_needed("City"), "City");
89        assert_eq!(quote_if_needed("Phone 1"), "\"Phone 1\"");
90        assert_eq!(quote_if_needed("Has\"Quote"), "\"Has\"\"Quote\"");
91    }
92}