Skip to main content

zerodds_sql_filter/
evaluator.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Expression-Evaluator.
5//!
6//! Die Evaluation ist strikt — Type-Mismatch (z.B. String < Int)
7//! liefert `EvalError::TypeMismatch`. Der Caller entscheidet, ob er
8//! das als `filter denies` oder als `filter error` behandelt.
9
10use alloc::string::String;
11
12use crate::ast::{CmpOp, Expr, Operand, Value};
13
14/// Row-Abstraktion: ein Zugriff auf die Felder einer Stichprobe.
15///
16/// Implementierer mappen dotted-Pfade (`a.b.c`) auf die passenden
17/// Werte. Für einfache flache Structs reicht ein `HashMap<String,
18/// Value>`-Lookup.
19pub trait RowAccess {
20    /// Liefert den Wert des Feldpfads, wenn vorhanden. `None` = kein
21    /// Feld mit dem Namen.
22    fn get(&self, path: &str) -> Option<Value>;
23}
24
25/// Fehler bei der Evaluation.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum EvalError {
28    /// Feld nicht gefunden.
29    UnknownField(String),
30    /// Parameter-Index ausserhalb des uebergebenen Slice.
31    MissingParam(u32),
32    /// Operator nicht kompatibel mit den Operand-Typen.
33    TypeMismatch(String),
34}
35
36impl core::fmt::Display for EvalError {
37    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38        match self {
39            Self::UnknownField(n) => write!(f, "unknown field: {n}"),
40            Self::MissingParam(i) => write!(f, "missing parameter %{i}"),
41            Self::TypeMismatch(m) => write!(f, "type mismatch: {m}"),
42        }
43    }
44}
45
46#[cfg(feature = "std")]
47impl std::error::Error for EvalError {}
48
49impl Expr {
50    /// Werte die Expression gegen einen Row + Parameter-Slice aus.
51    ///
52    /// # Errors
53    /// Siehe [`EvalError`].
54    pub fn evaluate<R: RowAccess>(&self, row: &R, params: &[Value]) -> Result<bool, EvalError> {
55        match self {
56            Self::And(a, b) => Ok(a.evaluate(row, params)? && b.evaluate(row, params)?),
57            Self::Or(a, b) => Ok(a.evaluate(row, params)? || b.evaluate(row, params)?),
58            Self::Not(inner) => Ok(!inner.evaluate(row, params)?),
59            Self::Cmp { lhs, op, rhs } => {
60                let l = resolve_operand(lhs, row, params)?;
61                let r = resolve_operand(rhs, row, params)?;
62                cmp(&l, *op, &r)
63            }
64            Self::Between {
65                field,
66                low,
67                high,
68                negated,
69            } => {
70                let f = resolve_operand(field, row, params)?;
71                let lo = resolve_operand(low, row, params)?;
72                let hi = resolve_operand(high, row, params)?;
73                let in_range = cmp(&f, CmpOp::Ge, &lo)? && cmp(&f, CmpOp::Le, &hi)?;
74                Ok(if *negated { !in_range } else { in_range })
75            }
76        }
77    }
78}
79
80fn resolve_operand<R: RowAccess>(
81    op: &Operand,
82    row: &R,
83    params: &[Value],
84) -> Result<Value, EvalError> {
85    match op {
86        Operand::Literal(v) => Ok(v.clone()),
87        Operand::Field(name) => row
88            .get(name)
89            .ok_or_else(|| EvalError::UnknownField(name.clone())),
90        Operand::Param(i) => params
91            .get(*i as usize)
92            .cloned()
93            .ok_or(EvalError::MissingParam(*i)),
94    }
95}
96
97fn cmp(lhs: &Value, op: CmpOp, rhs: &Value) -> Result<bool, EvalError> {
98    // Numerische Promotion fuer Int/Float-Vergleiche.
99    if let (Some(l), Some(r)) = (as_f64(lhs), as_f64(rhs)) {
100        return Ok(match op {
101            CmpOp::Eq => (l - r).abs() < f64::EPSILON,
102            CmpOp::Neq => (l - r).abs() >= f64::EPSILON,
103            CmpOp::Lt => l < r,
104            CmpOp::Le => l <= r,
105            CmpOp::Gt => l > r,
106            CmpOp::Ge => l >= r,
107            CmpOp::Like => {
108                return Err(EvalError::TypeMismatch("LIKE nur für String".into()));
109            }
110        });
111    }
112
113    match (lhs, rhs, op) {
114        (Value::String(a), Value::String(b), CmpOp::Eq) => Ok(a == b),
115        (Value::String(a), Value::String(b), CmpOp::Neq) => Ok(a != b),
116        (Value::String(a), Value::String(b), CmpOp::Lt) => Ok(a < b),
117        (Value::String(a), Value::String(b), CmpOp::Le) => Ok(a <= b),
118        (Value::String(a), Value::String(b), CmpOp::Gt) => Ok(a > b),
119        (Value::String(a), Value::String(b), CmpOp::Ge) => Ok(a >= b),
120        (Value::String(a), Value::String(b), CmpOp::Like) => Ok(like_match(a, b)),
121        (Value::Bool(a), Value::Bool(b), CmpOp::Eq) => Ok(a == b),
122        (Value::Bool(a), Value::Bool(b), CmpOp::Neq) => Ok(a != b),
123        (a, b, op) => Err(EvalError::TypeMismatch(alloc::format!(
124            "{a:?} {op:?} {b:?}"
125        ))),
126    }
127}
128
129fn as_f64(v: &Value) -> Option<f64> {
130    match v {
131        #[allow(clippy::cast_precision_loss)]
132        Value::Int(n) => Some(*n as f64),
133        Value::Float(f) => Some(*f),
134        _ => None,
135    }
136}
137
138/// SQL-92 LIKE-Match mit `%` (null-oder-mehr) und `_` (genau ein Zeichen).
139/// Backslash-Escape ist nicht implementiert — Spec §B.2.1 verlangt es
140/// nicht; %/_ in Daten muss der Caller per Doppel-Encoding einbringen.
141fn like_match(s: &str, pat: &str) -> bool {
142    // Klassisches DP: m[i][j] = s[..i] matcht pat[..j]
143    let s_chars: alloc::vec::Vec<char> = s.chars().collect();
144    let p_chars: alloc::vec::Vec<char> = pat.chars().collect();
145    let (m, n) = (s_chars.len(), p_chars.len());
146    let mut dp = alloc::vec![alloc::vec![false; n + 1]; m + 1];
147    dp[0][0] = true;
148    for j in 1..=n {
149        if p_chars[j - 1] == '%' {
150            dp[0][j] = dp[0][j - 1];
151        }
152    }
153    for i in 1..=m {
154        for j in 1..=n {
155            let pc = p_chars[j - 1];
156            dp[i][j] = if pc == '%' {
157                dp[i - 1][j] || dp[i][j - 1]
158            } else if pc == '_' || pc == s_chars[i - 1] {
159                dp[i - 1][j - 1]
160            } else {
161                false
162            };
163        }
164    }
165    dp[m][n]
166}
167
168#[cfg(test)]
169#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
170mod tests {
171    use super::*;
172    use crate::parser::parse;
173    use alloc::collections::BTreeMap;
174
175    struct MapRow(BTreeMap<String, Value>);
176    impl RowAccess for MapRow {
177        fn get(&self, path: &str) -> Option<Value> {
178            self.0.get(path).cloned()
179        }
180    }
181
182    fn row(pairs: &[(&str, Value)]) -> MapRow {
183        let mut m = BTreeMap::new();
184        for (k, v) in pairs {
185            m.insert((*k).into(), v.clone());
186        }
187        MapRow(m)
188    }
189
190    #[test]
191    fn evaluates_string_eq() {
192        let e = parse("color = 'RED'").unwrap();
193        let r = row(&[("color", Value::String("RED".into()))]);
194        assert_eq!(e.evaluate(&r, &[]), Ok(true));
195    }
196
197    #[test]
198    fn evaluates_int_compare() {
199        let e = parse("x > 10 AND x <= 100").unwrap();
200        let r = row(&[("x", Value::Int(42))]);
201        assert_eq!(e.evaluate(&r, &[]), Ok(true));
202    }
203
204    #[test]
205    fn evaluates_float_int_cross() {
206        // Int auf einer Seite, Float auf der anderen — promotet zu f64.
207        let e = parse("x < 3.5").unwrap();
208        let r = row(&[("x", Value::Int(3))]);
209        assert_eq!(e.evaluate(&r, &[]), Ok(true));
210    }
211
212    #[test]
213    fn evaluates_boolean_not_or() {
214        let e = parse("NOT (x = 0 OR y = 0)").unwrap();
215        let r = row(&[("x", Value::Int(1)), ("y", Value::Int(2))]);
216        assert_eq!(e.evaluate(&r, &[]), Ok(true));
217    }
218
219    #[test]
220    fn evaluates_param() {
221        let e = parse("color = %0").unwrap();
222        let r = row(&[("color", Value::String("BLUE".into()))]);
223        assert_eq!(e.evaluate(&r, &[Value::String("BLUE".into())]), Ok(true),);
224    }
225
226    #[test]
227    fn missing_param_is_error() {
228        let e = parse("color = %0").unwrap();
229        let r = row(&[("color", Value::String("BLUE".into()))]);
230        assert_eq!(e.evaluate(&r, &[]), Err(EvalError::MissingParam(0)),);
231    }
232
233    #[test]
234    fn unknown_field_is_error() {
235        let e = parse("missing = 1").unwrap();
236        let r = row(&[("x", Value::Int(1))]);
237        assert!(matches!(
238            e.evaluate(&r, &[]),
239            Err(EvalError::UnknownField(_))
240        ));
241    }
242
243    #[test]
244    fn like_wildcards() {
245        let e = parse("name LIKE 'foo%'").unwrap();
246        let r_yes = row(&[("name", Value::String("foobar".into()))]);
247        let r_no = row(&[("name", Value::String("barfoo".into()))]);
248        assert_eq!(e.evaluate(&r_yes, &[]), Ok(true));
249        assert_eq!(e.evaluate(&r_no, &[]), Ok(false));
250
251        let single = parse("name LIKE 'a_c'").unwrap();
252        let r_yes = row(&[("name", Value::String("abc".into()))]);
253        let r_no = row(&[("name", Value::String("abbc".into()))]);
254        assert_eq!(single.evaluate(&r_yes, &[]), Ok(true));
255        assert_eq!(single.evaluate(&r_no, &[]), Ok(false));
256    }
257
258    #[test]
259    fn like_on_non_string_rejected() {
260        let e = parse("x LIKE 5").unwrap();
261        let r = row(&[("x", Value::Int(5))]);
262        assert!(matches!(
263            e.evaluate(&r, &[]),
264            Err(EvalError::TypeMismatch(_))
265        ));
266    }
267}