Skip to main content

featherdb_query/expr/
helpers.rs

1//! Helper functions for expression evaluation
2
3use std::collections::HashSet;
4
5/// Match text against a SQL LIKE pattern
6///
7/// Supports:
8/// - `%` matches zero or more characters
9/// - `_` matches exactly one character
10pub fn like_match(text: &str, pattern: &str) -> bool {
11    let text_chars: Vec<char> = text.chars().collect();
12    let pattern_chars: Vec<char> = pattern.chars().collect();
13
14    like_match_recursive(&text_chars, &pattern_chars, 0, 0)
15}
16
17fn like_match_recursive(
18    text: &[char],
19    pattern: &[char],
20    text_idx: usize,
21    pattern_idx: usize,
22) -> bool {
23    // Base cases
24    if pattern_idx == pattern.len() {
25        return text_idx == text.len();
26    }
27
28    // Handle wildcard %
29    if pattern[pattern_idx] == '%' {
30        // Try matching zero characters
31        if like_match_recursive(text, pattern, text_idx, pattern_idx + 1) {
32            return true;
33        }
34        // Try matching one or more characters
35        if text_idx < text.len() {
36            return like_match_recursive(text, pattern, text_idx + 1, pattern_idx);
37        }
38        return false;
39    }
40
41    // Check if we've consumed all text but pattern remains
42    if text_idx >= text.len() {
43        return false;
44    }
45
46    // Handle single character wildcard _
47    if pattern[pattern_idx] == '_' {
48        return like_match_recursive(text, pattern, text_idx + 1, pattern_idx + 1);
49    }
50
51    // Match literal character
52    if text[text_idx] == pattern[pattern_idx] {
53        return like_match_recursive(text, pattern, text_idx + 1, pattern_idx + 1);
54    }
55
56    false
57}
58
59/// Collect all column references from an expression tree
60pub fn collect_column_refs(expr: &super::Expr, columns: &mut HashSet<String>) {
61    match expr {
62        super::Expr::Column { name, .. } => {
63            columns.insert(name.clone());
64        }
65        super::Expr::BinaryOp { left, right, .. } => {
66            collect_column_refs(left, columns);
67            collect_column_refs(right, columns);
68        }
69        super::Expr::UnaryOp { expr, .. } => {
70            collect_column_refs(expr, columns);
71        }
72        super::Expr::Function { args, .. } => {
73            for arg in args {
74                collect_column_refs(arg, columns);
75            }
76        }
77        super::Expr::Aggregate { arg, .. } => {
78            if let Some(e) = arg {
79                collect_column_refs(e, columns);
80            }
81        }
82        super::Expr::Case {
83            when_clauses,
84            else_result,
85            ..
86        } => {
87            for (cond, res) in when_clauses {
88                collect_column_refs(cond, columns);
89                collect_column_refs(res, columns);
90            }
91            if let Some(else_expr) = else_result {
92                collect_column_refs(else_expr, columns);
93            }
94        }
95        super::Expr::InList { expr, list, .. } => {
96            collect_column_refs(expr, columns);
97            for item in list {
98                collect_column_refs(item, columns);
99            }
100        }
101        super::Expr::Between {
102            expr, low, high, ..
103        } => {
104            collect_column_refs(expr, columns);
105            collect_column_refs(low, columns);
106            collect_column_refs(high, columns);
107        }
108        super::Expr::Window { .. } => {
109            // Window functions handle their own column references
110        }
111        super::Expr::ScalarSubquery { .. }
112        | super::Expr::Exists { .. }
113        | super::Expr::InSubquery { .. }
114        | super::Expr::AnySubquery { .. }
115        | super::Expr::AllSubquery { .. } => {
116            // Subqueries handle their own column references
117        }
118        super::Expr::Literal(_)
119        | super::Expr::Parameter { .. }
120        | super::Expr::Wildcard
121        | super::Expr::QualifiedWildcard(_)
122        | super::Expr::Like { .. } => {
123            // No column references
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_like_match_exact() {
134        assert!(like_match("hello", "hello"));
135        assert!(!like_match("hello", "world"));
136    }
137
138    #[test]
139    fn test_like_match_percent() {
140        assert!(like_match("hello", "h%"));
141        assert!(like_match("hello", "%lo"));
142        assert!(like_match("hello", "h%o"));
143        assert!(like_match("hello", "%"));
144        assert!(!like_match("hello", "w%"));
145    }
146
147    #[test]
148    fn test_like_match_underscore() {
149        assert!(like_match("hello", "h_llo"));
150        assert!(like_match("hello", "_ello"));
151        assert!(like_match("hello", "hell_"));
152        assert!(!like_match("hello", "h_lo"));
153    }
154
155    #[test]
156    fn test_like_match_combined() {
157        assert!(like_match("hello world", "h%d"));
158        assert!(like_match("hello world", "h_llo%"));
159        assert!(like_match("hello world", "%o w%"));
160        assert!(!like_match("hello world", "h%x"));
161    }
162
163    #[test]
164    fn test_like_match_edge_cases() {
165        assert!(like_match("", ""));
166        assert!(like_match("", "%"));
167        assert!(!like_match("", "_"));
168        assert!(like_match("a", "_"));
169        assert!(!like_match("ab", "_"));
170    }
171
172    #[test]
173    fn test_collect_column_refs_literal() {
174        use super::super::Expr;
175        use featherdb_core::Value;
176
177        let expr = Expr::Literal(Value::Integer(42));
178        let mut columns = HashSet::new();
179        collect_column_refs(&expr, &mut columns);
180        assert!(columns.is_empty());
181    }
182
183    #[test]
184    fn test_collect_column_refs_column() {
185        use super::super::Expr;
186
187        let expr = Expr::Column {
188            table: None,
189            name: "age".to_string(),
190            index: None,
191        };
192        let mut columns = HashSet::new();
193        collect_column_refs(&expr, &mut columns);
194        assert_eq!(columns.len(), 1);
195        assert!(columns.contains("age"));
196    }
197
198    #[test]
199    fn test_collect_column_refs_binary_op() {
200        use super::super::{BinaryOp, Expr};
201
202        let expr = Expr::BinaryOp {
203            left: Box::new(Expr::Column {
204                table: None,
205                name: "age".to_string(),
206                index: None,
207            }),
208            op: BinaryOp::Gt,
209            right: Box::new(Expr::Column {
210                table: None,
211                name: "salary".to_string(),
212                index: None,
213            }),
214        };
215        let mut columns = HashSet::new();
216        collect_column_refs(&expr, &mut columns);
217        assert_eq!(columns.len(), 2);
218        assert!(columns.contains("age"));
219        assert!(columns.contains("salary"));
220    }
221
222    #[test]
223    fn test_collect_column_refs_qualified_column() {
224        use super::super::Expr;
225
226        let expr = Expr::Column {
227            table: Some("users".to_string()),
228            name: "name".to_string(),
229            index: None,
230        };
231        let mut columns = HashSet::new();
232        collect_column_refs(&expr, &mut columns);
233        assert_eq!(columns.len(), 1);
234        assert!(columns.contains("name"));
235    }
236}