Skip to main content

nodedb_query/scan_filter/
parse.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use nodedb_types::find_ascii_case_insensitive;
4
5use super::{FilterOp, ScanFilter};
6
7/// Parse simple SQL predicates into `ScanFilter` values.
8///
9/// Handles basic `field op value` predicates joined by AND.
10/// Supports: `=`, `!=`, `<>`, `>`, `>=`, `<`, `<=`, `LIKE`, `ILIKE`.
11/// Values: single-quoted strings, numbers, `TRUE`/`FALSE`, `NULL`.
12///
13/// For complex predicates (OR, subqueries, functions), returns empty vec
14/// (match all — facet counts will be unfiltered).
15pub fn parse_simple_predicates(text: &str) -> Vec<ScanFilter> {
16    let mut filters = Vec::new();
17    for clause in text.split(" AND ").flat_map(|s| s.split(" and ")) {
18        let clause = clause.trim();
19        if clause.is_empty() {
20            continue;
21        }
22        if let Some(f) = parse_single_predicate(clause) {
23            filters.push(f);
24        }
25    }
26    filters
27}
28
29fn parse_single_predicate(clause: &str) -> Option<ScanFilter> {
30    let ops = &[">=", "<=", "!=", "<>", "=", ">", "<"];
31    for op_str in ops {
32        if let Some(pos) = clause.find(op_str) {
33            let field = clause[..pos].trim().to_string();
34            let raw_value = clause[pos + op_str.len()..].trim();
35            let op = match *op_str {
36                "=" => FilterOp::Eq,
37                "!=" | "<>" => FilterOp::Ne,
38                ">" => FilterOp::Gt,
39                ">=" => FilterOp::Gte,
40                "<" => FilterOp::Lt,
41                "<=" => FilterOp::Lte,
42                _ => return None,
43            };
44            return Some(ScanFilter {
45                field,
46                op,
47                value: nodedb_types::Value::from(parse_predicate_value(raw_value)),
48                clauses: Vec::new(),
49                expr: None,
50            });
51        }
52    }
53
54    if let Some(pos) = find_ascii_case_insensitive(clause, " LIKE ") {
55        let field = clause[..pos].trim().to_string();
56        let raw_value = clause[pos + 6..].trim();
57        return Some(ScanFilter {
58            field,
59            op: FilterOp::Like,
60            value: nodedb_types::Value::from(parse_predicate_value(raw_value)),
61            clauses: Vec::new(),
62            expr: None,
63        });
64    }
65    if let Some(pos) = find_ascii_case_insensitive(clause, " ILIKE ") {
66        let field = clause[..pos].trim().to_string();
67        let raw_value = clause[pos + 7..].trim();
68        return Some(ScanFilter {
69            field,
70            op: FilterOp::Ilike,
71            value: nodedb_types::Value::from(parse_predicate_value(raw_value)),
72            clauses: Vec::new(),
73            expr: None,
74        });
75    }
76
77    None
78}
79
80fn parse_predicate_value(raw: &str) -> serde_json::Value {
81    let raw = raw.trim();
82    if let Some(inner) = raw
83        .strip_prefix('\'')
84        .and_then(|value| value.strip_suffix('\''))
85    {
86        return serde_json::Value::String(inner.replace("''", "'"));
87    }
88    if raw.eq_ignore_ascii_case("TRUE") {
89        return serde_json::Value::Bool(true);
90    }
91    if raw.eq_ignore_ascii_case("FALSE") {
92        return serde_json::Value::Bool(false);
93    }
94    if raw.eq_ignore_ascii_case("NULL") {
95        return serde_json::Value::Null;
96    }
97    if let Ok(i) = raw.parse::<i64>() {
98        return serde_json::json!(i);
99    }
100    if let Ok(f) = raw.parse::<f64>() {
101        return serde_json::json!(f);
102    }
103    serde_json::Value::String(raw.to_string())
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn unicode_before_like_preserves_original_byte_offsets() {
112        let filters = parse_simple_predicates("Straße LIKE 'ß%'");
113        assert_eq!(filters.len(), 1);
114        assert_eq!(filters[0].field, "Straße");
115        assert_eq!(filters[0].value, nodedb_types::Value::String("ß%".into()));
116
117        let filters = parse_simple_predicates("İstanbul ILIKE 'İ%'");
118        assert_eq!(filters.len(), 1);
119        assert_eq!(filters[0].field, "İstanbul");
120        assert_eq!(filters[0].value, nodedb_types::Value::String("İ%".into()));
121    }
122}