Skip to main content

dynoxide/expressions/
condition.rs

1//! ConditionExpression and FilterExpression parsing and evaluation.
2//!
3//! Both use identical syntax: comparisons, functions, BETWEEN, IN, AND/OR/NOT.
4
5use crate::expressions::tokenizer::{Token, TokenStream, tokenize};
6use crate::expressions::{
7    PathElement, TrackedExpressionAttributes, resolve_path, resolve_path_elements,
8};
9use crate::types::AttributeValue;
10use std::collections::HashMap;
11
12/// Parsed condition expression AST.
13#[derive(Debug, Clone)]
14pub enum ConditionExpr {
15    /// `path comparator operand`
16    Comparison {
17        left: Operand,
18        op: CompOp,
19        right: Operand,
20    },
21    /// `operand BETWEEN lo AND hi`
22    Between {
23        operand: Operand,
24        lo: Operand,
25        hi: Operand,
26    },
27    /// `operand IN (val1, val2, ...)`
28    In {
29        operand: Operand,
30        values: Vec<Operand>,
31    },
32    /// `attribute_exists(path)`
33    AttributeExists(Vec<PathElement>),
34    /// `attribute_not_exists(path)`
35    AttributeNotExists(Vec<PathElement>),
36    /// `attribute_type(path, :type_val)`
37    AttributeType(Vec<PathElement>, Operand),
38    /// `begins_with(path, operand)`
39    BeginsWith(Operand, Operand),
40    /// `contains(path, operand)`
41    Contains(Operand, Operand),
42    /// `size(path)` — used as operand in comparisons, handled specially
43    /// This is actually an operand, not standalone. We handle `size()` as an Operand variant.
44
45    /// `expr AND expr`
46    And(Box<ConditionExpr>, Box<ConditionExpr>),
47    /// `expr OR expr`
48    Or(Box<ConditionExpr>, Box<ConditionExpr>),
49    /// `NOT expr`
50    Not(Box<ConditionExpr>),
51}
52
53/// Comparison operators.
54#[derive(Debug, Clone, PartialEq)]
55pub enum CompOp {
56    Eq,
57    Ne,
58    Lt,
59    Le,
60    Gt,
61    Ge,
62}
63
64/// An operand in an expression.
65#[derive(Debug, Clone)]
66pub enum Operand {
67    /// A document path (e.g., `attr`, `a.b[0].c`, `#name.sub`)
68    Path(Vec<PathElement>),
69    /// A value reference (`:val`)
70    ValueRef(String),
71    /// `size(path)` function used as an operand
72    Size(Vec<PathElement>),
73}
74
75/// Parse a condition/filter expression string.
76///
77/// Errors returned are the raw error text without the "Invalid FilterExpression: " etc.
78/// prefix — callers must add the appropriate prefix for their expression type.
79pub fn parse(expr: &str) -> Result<ConditionExpr, String> {
80    let tokens = tokenize(expr).map_err(|e| e.to_string())?;
81    let mut stream = TokenStream::new(tokens);
82    let result = parse_or(&mut stream)?;
83    if !stream.at_end() {
84        return Err(format!(
85            "Syntax error; token: \"{}\"",
86            stream.peek().unwrap()
87        ));
88    }
89    Ok(result)
90}
91
92/// Evaluate a condition expression against an item, using a `TrackedExpressionAttributes`
93/// to resolve and track which names/values are referenced.
94pub fn evaluate(
95    expr: &ConditionExpr,
96    item: &HashMap<String, AttributeValue>,
97    tracker: &TrackedExpressionAttributes,
98) -> Result<bool, String> {
99    match expr {
100        ConditionExpr::Comparison { left, op, right } => {
101            let lv = resolve_operand(left, item, tracker)?;
102            let rv = resolve_operand(right, item, tracker)?;
103            match (lv, rv) {
104                (Some(l), Some(r)) => Ok(compare_values(&l, op, &r)),
105                _ => Ok(false),
106            }
107        }
108
109        ConditionExpr::Between { operand, lo, hi } => {
110            let val = resolve_operand(operand, item, tracker)?;
111            let lo_val = resolve_operand(lo, item, tracker)?;
112            let hi_val = resolve_operand(hi, item, tracker)?;
113            match (val, lo_val, hi_val) {
114                (Some(v), Some(l), Some(h)) => {
115                    Ok(compare_values(&v, &CompOp::Ge, &l) && compare_values(&v, &CompOp::Le, &h))
116                }
117                _ => Ok(false),
118            }
119        }
120
121        ConditionExpr::In { operand, values } => {
122            let val = resolve_operand(operand, item, tracker)?;
123            match val {
124                Some(v) => {
125                    for candidate in values {
126                        let cv = resolve_operand(candidate, item, tracker)?;
127                        if let Some(c) = cv {
128                            if compare_values(&v, &CompOp::Eq, &c) {
129                                return Ok(true);
130                            }
131                        }
132                    }
133                    Ok(false)
134                }
135                None => Ok(false),
136            }
137        }
138
139        ConditionExpr::AttributeExists(path) => {
140            let resolved = resolve_path_elements(path, tracker)?;
141            Ok(resolve_path(item, &resolved).is_some())
142        }
143
144        ConditionExpr::AttributeNotExists(path) => {
145            let resolved = resolve_path_elements(path, tracker)?;
146            Ok(resolve_path(item, &resolved).is_none())
147        }
148
149        ConditionExpr::AttributeType(path, type_operand) => {
150            let resolved = resolve_path_elements(path, tracker)?;
151            let val = resolve_path(item, &resolved);
152            let type_val = resolve_operand(type_operand, item, tracker)?;
153            match (val, type_val) {
154                (Some(v), Some(AttributeValue::S(type_name))) => Ok(v.type_name() == type_name),
155                _ => Ok(false),
156            }
157        }
158
159        ConditionExpr::BeginsWith(path_op, prefix_op) => {
160            let val = resolve_operand(path_op, item, tracker)?;
161            let prefix = resolve_operand(prefix_op, item, tracker)?;
162            match (val, prefix) {
163                (Some(AttributeValue::S(s)), Some(AttributeValue::S(p))) => Ok(s.starts_with(&p)),
164                (Some(AttributeValue::B(b)), Some(AttributeValue::B(p))) => Ok(b.starts_with(&p)),
165                _ => Ok(false),
166            }
167        }
168
169        ConditionExpr::Contains(path_op, search_op) => {
170            let val = resolve_operand(path_op, item, tracker)?;
171            let search = resolve_operand(search_op, item, tracker)?;
172            match (val, search) {
173                (Some(AttributeValue::S(s)), Some(AttributeValue::S(sub))) => Ok(s.contains(&sub)),
174                (Some(AttributeValue::B(b)), Some(AttributeValue::B(sub))) => {
175                    Ok(sub.is_empty() || b.windows(sub.len()).any(|w| w == sub.as_slice()))
176                }
177                (Some(AttributeValue::SS(set)), Some(AttributeValue::S(elem))) => {
178                    Ok(set.contains(&elem))
179                }
180                (Some(AttributeValue::NS(set)), Some(AttributeValue::N(elem))) => {
181                    Ok(set.contains(&elem))
182                }
183                (Some(AttributeValue::BS(set)), Some(AttributeValue::B(elem))) => {
184                    Ok(set.contains(&elem))
185                }
186                (Some(AttributeValue::L(list)), Some(search_val)) => Ok(list
187                    .iter()
188                    .any(|v| compare_values(v, &CompOp::Eq, &search_val))),
189                _ => Ok(false),
190            }
191        }
192
193        ConditionExpr::And(left, right) => {
194            if !evaluate(left, item, tracker)? {
195                return Ok(false); // short-circuit
196            }
197            evaluate(right, item, tracker)
198        }
199
200        ConditionExpr::Or(left, right) => {
201            if evaluate(left, item, tracker)? {
202                return Ok(true); // short-circuit
203            }
204            evaluate(right, item, tracker)
205        }
206
207        ConditionExpr::Not(inner) => {
208            let v = evaluate(inner, item, tracker)?;
209            Ok(!v)
210        }
211    }
212}
213
214// ---------------------------------------------------------------------------
215// Parser (recursive descent with precedence: OR < AND < NOT < comparison)
216// ---------------------------------------------------------------------------
217
218fn parse_or(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
219    let mut left = parse_and(stream)?;
220    while matches!(stream.peek(), Some(Token::Or)) {
221        stream.next();
222        let right = parse_and(stream)?;
223        left = ConditionExpr::Or(Box::new(left), Box::new(right));
224    }
225    Ok(left)
226}
227
228fn parse_and(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
229    let mut left = parse_not(stream)?;
230    while matches!(stream.peek(), Some(Token::And)) {
231        stream.next();
232        let right = parse_not(stream)?;
233        left = ConditionExpr::And(Box::new(left), Box::new(right));
234    }
235    Ok(left)
236}
237
238fn parse_not(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
239    if matches!(stream.peek(), Some(Token::Not)) {
240        stream.next();
241        let inner = parse_not(stream)?;
242        return Ok(ConditionExpr::Not(Box::new(inner)));
243    }
244    parse_primary(stream)
245}
246
247fn parse_primary(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
248    // Parenthesized expression
249    if matches!(stream.peek(), Some(Token::LParen)) {
250        stream.next();
251        let expr = parse_or(stream)?;
252        stream.expect(&Token::RParen)?;
253        return Ok(expr);
254    }
255
256    // Check for function calls
257    if let Some(Token::Identifier(name)) = stream.peek() {
258        let name_owned = name.clone();
259        let func_name = name_owned.to_lowercase();
260
261        // Check if next token after identifier is '(' — if so, it's a function call
262        let is_function_call = {
263            let saved = stream.pos();
264            stream.next(); // consume identifier
265            let is_lparen = matches!(stream.peek(), Some(Token::LParen));
266            stream.set_pos(saved); // restore position
267            is_lparen
268        };
269
270        if is_function_call {
271            // Known condition-level functions (return bool, valid as primary expression)
272            match func_name.as_str() {
273                "attribute_exists" => {
274                    stream.next();
275                    stream.expect(&Token::LParen)?;
276                    let path = parse_raw_path(stream)?;
277                    stream.expect(&Token::RParen)?;
278                    return Ok(ConditionExpr::AttributeExists(path));
279                }
280                "attribute_not_exists" => {
281                    stream.next();
282                    stream.expect(&Token::LParen)?;
283                    let path = parse_raw_path(stream)?;
284                    stream.expect(&Token::RParen)?;
285                    return Ok(ConditionExpr::AttributeNotExists(path));
286                }
287                "attribute_type" => {
288                    stream.next();
289                    stream.expect(&Token::LParen)?;
290                    let path = parse_raw_path(stream)?;
291                    stream.expect(&Token::Comma)?;
292                    let type_val = parse_operand(stream)?;
293                    stream.expect(&Token::RParen)?;
294                    return Ok(ConditionExpr::AttributeType(path, type_val));
295                }
296                "begins_with" => {
297                    stream.next();
298                    stream.expect(&Token::LParen)?;
299                    let path_op = parse_operand(stream)?;
300                    stream.expect(&Token::Comma)?;
301                    let prefix_op = parse_operand(stream)?;
302                    stream.expect(&Token::RParen)?;
303                    return Ok(ConditionExpr::BeginsWith(path_op, prefix_op));
304                }
305                "contains" => {
306                    stream.next();
307                    stream.expect(&Token::LParen)?;
308                    let path_op = parse_operand(stream)?;
309                    stream.expect(&Token::Comma)?;
310                    let search_op = parse_operand(stream)?;
311                    stream.expect(&Token::RParen)?;
312                    return Ok(ConditionExpr::Contains(path_op, search_op));
313                }
314                "size" => {
315                    // size() is an operand-level function, not a condition-level function.
316                    // Fall through to comparison parsing where parse_operand handles it.
317                }
318                _ => {
319                    // Unknown function name
320                    return Err(format!("Invalid function name; function: {}", name_owned));
321                }
322            }
323        } else {
324            // Not a function call — fall through to comparison parsing.
325            // Reserved keyword check happens in parse_raw_path.
326        }
327    }
328
329    // Comparison: operand op operand, or operand BETWEEN, or operand IN
330    let left = parse_operand(stream)?;
331
332    match stream.peek() {
333        Some(Token::Eq) => {
334            stream.next();
335            let right = parse_operand(stream)?;
336            Ok(ConditionExpr::Comparison {
337                left,
338                op: CompOp::Eq,
339                right,
340            })
341        }
342        Some(Token::Ne) => {
343            stream.next();
344            let right = parse_operand(stream)?;
345            Ok(ConditionExpr::Comparison {
346                left,
347                op: CompOp::Ne,
348                right,
349            })
350        }
351        Some(Token::Lt) => {
352            stream.next();
353            let right = parse_operand(stream)?;
354            Ok(ConditionExpr::Comparison {
355                left,
356                op: CompOp::Lt,
357                right,
358            })
359        }
360        Some(Token::Le) => {
361            stream.next();
362            let right = parse_operand(stream)?;
363            Ok(ConditionExpr::Comparison {
364                left,
365                op: CompOp::Le,
366                right,
367            })
368        }
369        Some(Token::Gt) => {
370            stream.next();
371            let right = parse_operand(stream)?;
372            Ok(ConditionExpr::Comparison {
373                left,
374                op: CompOp::Gt,
375                right,
376            })
377        }
378        Some(Token::Ge) => {
379            stream.next();
380            let right = parse_operand(stream)?;
381            Ok(ConditionExpr::Comparison {
382                left,
383                op: CompOp::Ge,
384                right,
385            })
386        }
387        Some(Token::Between) => {
388            stream.next();
389            let lo = parse_operand(stream)?;
390            stream.expect(&Token::And)?;
391            let hi = parse_operand(stream)?;
392            Ok(ConditionExpr::Between {
393                operand: left,
394                lo,
395                hi,
396            })
397        }
398        Some(Token::In) => {
399            stream.next();
400            stream.expect(&Token::LParen)?;
401            let mut values = vec![parse_operand(stream)?];
402            while matches!(stream.peek(), Some(Token::Comma)) {
403                stream.next();
404                values.push(parse_operand(stream)?);
405            }
406            stream.expect(&Token::RParen)?;
407            Ok(ConditionExpr::In {
408                operand: left,
409                values,
410            })
411        }
412        _ => Err("Expected comparison operator, BETWEEN, or IN".to_string()),
413    }
414}
415
416/// Parse an operand (path, value ref, or size function).
417fn parse_operand(stream: &mut TokenStream) -> Result<Operand, String> {
418    // Check for size() function
419    if let Some(Token::Identifier(name)) = stream.peek() {
420        if name.to_lowercase() == "size" {
421            stream.next();
422            stream.expect(&Token::LParen)?;
423            let path = parse_raw_path(stream)?;
424            stream.expect(&Token::RParen)?;
425            return Ok(Operand::Size(path));
426        }
427    }
428
429    match stream.peek() {
430        Some(Token::ValueRef(_)) => {
431            if let Some(Token::ValueRef(name)) = stream.next().cloned() {
432                Ok(Operand::ValueRef(name))
433            } else {
434                unreachable!()
435            }
436        }
437        Some(Token::Identifier(_)) | Some(Token::NameRef(_)) => {
438            let path = parse_raw_path(stream)?;
439            Ok(Operand::Path(path))
440        }
441        Some(t) => Err(format!("Expected operand, got {t}")),
442        None => Err("Expected operand, got end of expression".to_string()),
443    }
444}
445
446/// Parse a raw document path (not resolving #names yet).
447/// Path format: `ident(.ident | [n])*`
448pub fn parse_raw_path(stream: &mut TokenStream) -> Result<Vec<PathElement>, String> {
449    let first = match stream.next() {
450        Some(Token::Identifier(name)) => {
451            if super::reserved::is_reserved_keyword(name) {
452                return Err(format!(
453                    "Attribute name is a reserved keyword; reserved keyword: {name}"
454                ));
455            }
456            PathElement::Attribute(name.clone())
457        }
458        Some(Token::NameRef(name)) => PathElement::Attribute(name.clone()),
459        Some(t) => return Err(format!("Expected attribute name, got {t}")),
460        None => return Err("Expected attribute name, got end of expression".to_string()),
461    };
462
463    let mut path = vec![first];
464
465    loop {
466        match stream.peek() {
467            Some(Token::Dot) => {
468                stream.next();
469                match stream.next() {
470                    Some(Token::Identifier(name)) => {
471                        if super::reserved::is_reserved_keyword(name) {
472                            return Err(format!(
473                                "Attribute name is a reserved keyword; reserved keyword: {name}"
474                            ));
475                        }
476                        path.push(PathElement::Attribute(name.clone()));
477                    }
478                    Some(Token::NameRef(name)) => {
479                        path.push(PathElement::Attribute(name.clone()));
480                    }
481                    Some(t) => return Err(format!("Expected attribute name after '.', got {t}")),
482                    None => return Err("Expected attribute name after '.'".to_string()),
483                }
484            }
485            Some(Token::LBracket) => {
486                stream.next();
487                match stream.next() {
488                    Some(Token::Number(n)) => {
489                        let idx: usize = n.parse().map_err(|_| format!("Invalid index: {n}"))?;
490                        path.push(PathElement::Index(idx));
491                    }
492                    Some(t) => return Err(format!("Expected number in brackets, got {t}")),
493                    None => return Err("Expected number in brackets".to_string()),
494                }
495                stream.expect(&Token::RBracket)?;
496            }
497            _ => break,
498        }
499    }
500
501    Ok(path)
502}
503
504// ---------------------------------------------------------------------------
505// Value resolution
506// ---------------------------------------------------------------------------
507
508/// Resolve an operand to an AttributeValue, tracking usage.
509fn resolve_operand(
510    operand: &Operand,
511    item: &HashMap<String, AttributeValue>,
512    tracker: &TrackedExpressionAttributes,
513) -> Result<Option<AttributeValue>, String> {
514    match operand {
515        Operand::Path(path) => {
516            let resolved = resolve_path_elements(path, tracker)?;
517            Ok(resolve_path(item, &resolved))
518        }
519        Operand::ValueRef(name) => {
520            let val = tracker.resolve_value(name)?;
521            Ok(Some(val.clone()))
522        }
523        Operand::Size(path) => {
524            let resolved = resolve_path_elements(path, tracker)?;
525            match resolve_path(item, &resolved) {
526                Some(val) => {
527                    let size = match &val {
528                        AttributeValue::S(s) => s.len(),
529                        AttributeValue::B(b) => b.len(),
530                        AttributeValue::SS(set) => set.len(),
531                        AttributeValue::NS(set) => set.len(),
532                        AttributeValue::BS(set) => set.len(),
533                        AttributeValue::L(list) => list.len(),
534                        AttributeValue::M(map) => map.len(),
535                        // N, BOOL, NULL do not support size() — return None
536                        // so the comparison evaluates to false (no match).
537                        _ => return Ok(None),
538                    };
539                    Ok(Some(AttributeValue::N(size.to_string())))
540                }
541                None => Ok(None),
542            }
543        }
544    }
545}
546
547// ---------------------------------------------------------------------------
548// Comparison logic
549// ---------------------------------------------------------------------------
550
551/// Compare two AttributeValues using a comparison operator.
552fn compare_values(left: &AttributeValue, op: &CompOp, right: &AttributeValue) -> bool {
553    match (left, right) {
554        // String comparisons
555        (AttributeValue::S(a), AttributeValue::S(b)) => compare_ord(a, b, op),
556
557        // Number comparisons — f64 fast-path for common cases, BigDecimal for edge cases
558        (AttributeValue::N(a), AttributeValue::N(b)) => {
559            // Fast path: f64 is exact for ≤15 significant digits with no scientific notation
560            if can_use_f64(a) && can_use_f64(b) {
561                if let (Ok(fa), Ok(fb)) = (a.parse::<f64>(), b.parse::<f64>()) {
562                    if fa.is_finite() && fb.is_finite() {
563                        return compare_ord(&fa, &fb, op);
564                    }
565                }
566            }
567            // Slow path: BigDecimal for 38-digit precision edge cases
568            use bigdecimal::BigDecimal;
569            use std::str::FromStr;
570            match (BigDecimal::from_str(a), BigDecimal::from_str(b)) {
571                (Ok(da), Ok(db)) => compare_ord(&da, &db, op),
572                _ => false,
573            }
574        }
575
576        // Binary comparisons
577        (AttributeValue::B(a), AttributeValue::B(b)) => compare_ord(a, b, op),
578
579        // Bool — only equality
580        (AttributeValue::BOOL(a), AttributeValue::BOOL(b)) => match op {
581            CompOp::Eq => a == b,
582            CompOp::Ne => a != b,
583            _ => false,
584        },
585
586        // Null — only equality
587        (AttributeValue::NULL(a), AttributeValue::NULL(b)) => match op {
588            CompOp::Eq => a == b,
589            CompOp::Ne => a != b,
590            _ => false,
591        },
592
593        // String Set — set equality (order-independent)
594        (AttributeValue::SS(a), AttributeValue::SS(b)) => {
595            let mut sa = a.clone();
596            let mut sb = b.clone();
597            sa.sort();
598            sb.sort();
599            match op {
600                CompOp::Eq => sa == sb,
601                CompOp::Ne => sa != sb,
602                _ => false,
603            }
604        }
605
606        // Number Set — set equality (order-independent)
607        (AttributeValue::NS(a), AttributeValue::NS(b)) => {
608            if a.len() != b.len() {
609                return matches!(op, CompOp::Ne);
610            }
611            let mut fa: Vec<f64> = match a.iter().map(|n| n.parse::<f64>()).collect() {
612                Ok(v) => v,
613                Err(_) => return false,
614            };
615            let mut fb: Vec<f64> = match b.iter().map(|n| n.parse::<f64>()).collect() {
616                Ok(v) => v,
617                Err(_) => return false,
618            };
619            fa.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
620            fb.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
621            match op {
622                CompOp::Eq => fa == fb,
623                CompOp::Ne => fa != fb,
624                _ => false,
625            }
626        }
627
628        // Binary Set — set equality (order-independent)
629        (AttributeValue::BS(a), AttributeValue::BS(b)) => {
630            let mut sa = a.clone();
631            let mut sb = b.clone();
632            sa.sort();
633            sb.sort();
634            match op {
635                CompOp::Eq => sa == sb,
636                CompOp::Ne => sa != sb,
637                _ => false,
638            }
639        }
640
641        // Different types — only <> is true
642        _ => matches!(op, CompOp::Ne),
643    }
644}
645
646fn compare_ord<T: PartialOrd>(a: &T, b: &T, op: &CompOp) -> bool {
647    match op {
648        CompOp::Eq => a == b,
649        CompOp::Ne => a != b,
650        CompOp::Lt => a < b,
651        CompOp::Le => a <= b,
652        CompOp::Gt => a > b,
653        CompOp::Ge => a >= b,
654    }
655}
656
657/// Walk a condition expression and track all attribute name and value references
658/// without evaluating. Used for pre-validation to detect unused names/values.
659pub fn track_references(
660    expr: &ConditionExpr,
661    tracker: &TrackedExpressionAttributes,
662) -> Result<(), String> {
663    match expr {
664        ConditionExpr::Comparison { left, op: _, right } => {
665            track_operand_refs(left, tracker)?;
666            track_operand_refs(right, tracker)
667        }
668        ConditionExpr::Between { operand, lo, hi } => {
669            track_operand_refs(operand, tracker)?;
670            track_operand_refs(lo, tracker)?;
671            track_operand_refs(hi, tracker)
672        }
673        ConditionExpr::In { operand, values } => {
674            track_operand_refs(operand, tracker)?;
675            for v in values {
676                track_operand_refs(v, tracker)?;
677            }
678            Ok(())
679        }
680        ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
681            track_cond_path_refs(path, tracker)
682        }
683        ConditionExpr::AttributeType(path, type_op) => {
684            track_cond_path_refs(path, tracker)?;
685            track_operand_refs(type_op, tracker)
686        }
687        ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
688            track_operand_refs(a, tracker)?;
689            track_operand_refs(b, tracker)
690        }
691        ConditionExpr::And(left, right) | ConditionExpr::Or(left, right) => {
692            track_references(left, tracker)?;
693            track_references(right, tracker)
694        }
695        ConditionExpr::Not(inner) => track_references(inner, tracker),
696    }
697}
698
699fn track_operand_refs(
700    operand: &Operand,
701    tracker: &TrackedExpressionAttributes,
702) -> Result<(), String> {
703    match operand {
704        Operand::Path(path) => track_cond_path_refs(path, tracker),
705        Operand::ValueRef(name) => {
706            tracker.resolve_value(name)?;
707            Ok(())
708        }
709        Operand::Size(path) => track_cond_path_refs(path, tracker),
710    }
711}
712
713fn track_cond_path_refs(
714    path: &[PathElement],
715    tracker: &TrackedExpressionAttributes,
716) -> Result<(), String> {
717    for elem in path {
718        if let PathElement::Attribute(name) = elem {
719            if name.starts_with('#') {
720                tracker.resolve_name(name)?;
721            }
722        }
723    }
724    Ok(())
725}
726
727/// Statically validate a condition expression against ExpressionAttributeValues.
728///
729/// Checks BETWEEN operands for:
730/// - Same data type (lower and upper bound must have the same type)
731/// - Correct ordering (lower bound must not be greater than upper bound)
732///
733/// This validation happens before table lookup, matching DynamoDB behaviour.
734pub fn validate_static(
735    expr: &ConditionExpr,
736    values: &Option<HashMap<String, AttributeValue>>,
737) -> Result<(), String> {
738    match expr {
739        ConditionExpr::Between { operand: _, lo, hi } => {
740            // Only validate when both bounds are value refs
741            if let (Operand::ValueRef(lo_name), Operand::ValueRef(hi_name)) = (lo, hi) {
742                if let Some(vals) = values {
743                    let lo_val = vals.get(lo_name.as_str());
744                    let hi_val = vals.get(hi_name.as_str());
745                    if let (Some(lo_v), Some(hi_v)) = (lo_val, hi_val) {
746                        // Check same data type
747                        if std::mem::discriminant(lo_v) != std::mem::discriminant(hi_v) {
748                            return Err(format!(
749                                "Invalid ConditionExpression: The BETWEEN operator requires same data type for lower and upper bounds; \
750                                 lower bound operand: AttributeValue: {{{}}}, upper bound operand: AttributeValue: {{{}}}",
751                                format_av_for_error(lo_v),
752                                format_av_for_error(hi_v),
753                            ));
754                        }
755                        // Check ordering
756                        if compare_values(lo_v, &CompOp::Gt, hi_v) {
757                            return Err(format!(
758                                "Invalid ConditionExpression: The BETWEEN operator requires upper bound to be greater than or equal to lower bound; \
759                                 lower bound operand: AttributeValue: {{{}}}, upper bound operand: AttributeValue: {{{}}}",
760                                format_av_for_error(lo_v),
761                                format_av_for_error(hi_v),
762                            ));
763                        }
764                    }
765                }
766            }
767            Ok(())
768        }
769        ConditionExpr::And(left, right) | ConditionExpr::Or(left, right) => {
770            validate_static(left, values)?;
771            validate_static(right, values)
772        }
773        ConditionExpr::Not(inner) => validate_static(inner, values),
774        _ => Ok(()),
775    }
776}
777
778/// Format an AttributeValue for error messages (e.g., "S:hello", "N:42").
779fn format_av_for_error(av: &AttributeValue) -> String {
780    match av {
781        AttributeValue::S(s) => format!("S:{s}"),
782        AttributeValue::N(n) => format!("N:{n}"),
783        AttributeValue::B(b) => {
784            use base64::Engine;
785            format!("B:{}", base64::engine::general_purpose::STANDARD.encode(b))
786        }
787        AttributeValue::BOOL(b) => format!("BOOL:{b}"),
788        AttributeValue::NULL(_) => "NULL:true".to_string(),
789        AttributeValue::SS(set) => format!("SS:{set:?}"),
790        AttributeValue::NS(set) => format!("NS:{set:?}"),
791        AttributeValue::BS(_) => "BS:[...]".to_string(),
792        AttributeValue::L(_) => "L:[...]".to_string(),
793        AttributeValue::M(_) => "M:{...}".to_string(),
794    }
795}
796
797/// Check for non-scalar key access in an expression.
798///
799/// DynamoDB rejects expressions that use `.` (map lookup) or `[]` (list index) on
800/// key attributes. Returns the offending key attribute name if found.
801///
802/// `key_attrs` contains the effective key attribute names.
803/// `index_key_attrs` contains secondary index key attribute names (for "IndexKey:" prefix).
804pub fn check_non_scalar_key_access(
805    expr: &ConditionExpr,
806    attr_names: &Option<HashMap<String, String>>,
807    key_attrs: &[String],
808    index_key_attrs: &[String],
809) -> Option<(String, bool)> {
810    // Returns (attr_name, is_index_key)
811    let mut result = None;
812    check_non_scalar_key_access_inner(expr, attr_names, key_attrs, index_key_attrs, &mut result);
813    result
814}
815
816fn check_non_scalar_key_access_inner(
817    expr: &ConditionExpr,
818    attr_names: &Option<HashMap<String, String>>,
819    key_attrs: &[String],
820    index_key_attrs: &[String],
821    result: &mut Option<(String, bool)>,
822) {
823    if result.is_some() {
824        return;
825    }
826    match expr {
827        ConditionExpr::Comparison { left, right, .. } => {
828            check_operand_non_scalar(left, attr_names, key_attrs, index_key_attrs, result);
829            check_operand_non_scalar(right, attr_names, key_attrs, index_key_attrs, result);
830        }
831        ConditionExpr::Between { operand, lo, hi } => {
832            check_operand_non_scalar(operand, attr_names, key_attrs, index_key_attrs, result);
833            check_operand_non_scalar(lo, attr_names, key_attrs, index_key_attrs, result);
834            check_operand_non_scalar(hi, attr_names, key_attrs, index_key_attrs, result);
835        }
836        ConditionExpr::In { operand, values } => {
837            check_operand_non_scalar(operand, attr_names, key_attrs, index_key_attrs, result);
838            for v in values {
839                check_operand_non_scalar(v, attr_names, key_attrs, index_key_attrs, result);
840            }
841        }
842        ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
843            check_path_non_scalar(path, attr_names, key_attrs, index_key_attrs, result);
844        }
845        ConditionExpr::AttributeType(path, _) => {
846            check_path_non_scalar(path, attr_names, key_attrs, index_key_attrs, result);
847        }
848        ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
849            check_operand_non_scalar(a, attr_names, key_attrs, index_key_attrs, result);
850            check_operand_non_scalar(b, attr_names, key_attrs, index_key_attrs, result);
851        }
852        ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
853            check_non_scalar_key_access_inner(a, attr_names, key_attrs, index_key_attrs, result);
854            check_non_scalar_key_access_inner(b, attr_names, key_attrs, index_key_attrs, result);
855        }
856        ConditionExpr::Not(inner) => {
857            check_non_scalar_key_access_inner(
858                inner,
859                attr_names,
860                key_attrs,
861                index_key_attrs,
862                result,
863            );
864        }
865    }
866}
867
868fn check_operand_non_scalar(
869    operand: &Operand,
870    attr_names: &Option<HashMap<String, String>>,
871    key_attrs: &[String],
872    index_key_attrs: &[String],
873    result: &mut Option<(String, bool)>,
874) {
875    if result.is_some() {
876        return;
877    }
878    match operand {
879        Operand::Path(path) | Operand::Size(path) => {
880            check_path_non_scalar(path, attr_names, key_attrs, index_key_attrs, result);
881        }
882        Operand::ValueRef(_) => {}
883    }
884}
885
886fn check_path_non_scalar(
887    path: &[PathElement],
888    attr_names: &Option<HashMap<String, String>>,
889    key_attrs: &[String],
890    index_key_attrs: &[String],
891    result: &mut Option<(String, bool)>,
892) {
893    if result.is_some() || path.len() <= 1 {
894        return; // single-element paths are fine (scalar access)
895    }
896    if let Some(name) = resolve_top_level_path(path, attr_names) {
897        if key_attrs.contains(&name) {
898            *result = Some((name, false));
899        } else if index_key_attrs.contains(&name) {
900            *result = Some((name, true));
901        }
902    }
903}
904
905/// Extract the top-level attribute names referenced in a condition expression.
906///
907/// Resolves `#name` references using `expression_attribute_names`.
908/// For paths like `a.b.c` or `a[1]`, only the root attribute `a` is returned.
909/// This is used for checking that FilterExpression doesn't reference key attributes.
910pub fn extract_top_level_attributes(
911    expr: &ConditionExpr,
912    attr_names: &Option<HashMap<String, String>>,
913) -> Vec<String> {
914    let mut attrs = Vec::new();
915    collect_top_level_attrs(expr, attr_names, &mut attrs);
916    attrs.sort();
917    attrs.dedup();
918    attrs
919}
920
921fn collect_top_level_attrs(
922    expr: &ConditionExpr,
923    attr_names: &Option<HashMap<String, String>>,
924    out: &mut Vec<String>,
925) {
926    match expr {
927        ConditionExpr::Comparison { left, right, .. } => {
928            collect_operand_top_attr(left, attr_names, out);
929            collect_operand_top_attr(right, attr_names, out);
930        }
931        ConditionExpr::Between { operand, lo, hi } => {
932            collect_operand_top_attr(operand, attr_names, out);
933            collect_operand_top_attr(lo, attr_names, out);
934            collect_operand_top_attr(hi, attr_names, out);
935        }
936        ConditionExpr::In { operand, values } => {
937            collect_operand_top_attr(operand, attr_names, out);
938            for v in values {
939                collect_operand_top_attr(v, attr_names, out);
940            }
941        }
942        ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
943            if let Some(name) = resolve_top_level_path(path, attr_names) {
944                out.push(name);
945            }
946        }
947        ConditionExpr::AttributeType(path, _) => {
948            if let Some(name) = resolve_top_level_path(path, attr_names) {
949                out.push(name);
950            }
951        }
952        ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
953            collect_operand_top_attr(a, attr_names, out);
954            collect_operand_top_attr(b, attr_names, out);
955        }
956        ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
957            collect_top_level_attrs(a, attr_names, out);
958            collect_top_level_attrs(b, attr_names, out);
959        }
960        ConditionExpr::Not(inner) => {
961            collect_top_level_attrs(inner, attr_names, out);
962        }
963    }
964}
965
966fn collect_operand_top_attr(
967    operand: &Operand,
968    attr_names: &Option<HashMap<String, String>>,
969    out: &mut Vec<String>,
970) {
971    match operand {
972        Operand::Path(path) => {
973            if let Some(name) = resolve_top_level_path(path, attr_names) {
974                out.push(name);
975            }
976        }
977        Operand::Size(path) => {
978            if let Some(name) = resolve_top_level_path(path, attr_names) {
979                out.push(name);
980            }
981        }
982        Operand::ValueRef(_) => {}
983    }
984}
985
986fn resolve_top_level_path(
987    path: &[PathElement],
988    attr_names: &Option<HashMap<String, String>>,
989) -> Option<String> {
990    match path.first() {
991        Some(PathElement::Attribute(name)) => {
992            if name.starts_with('#') {
993                attr_names
994                    .as_ref()
995                    .and_then(|m| m.get(name.as_str()))
996                    .cloned()
997            } else {
998                Some(name.clone())
999            }
1000        }
1001        _ => None,
1002    }
1003}
1004
1005/// Validate that all `#name` references in a condition expression are defined
1006/// in the provided `ExpressionAttributeNames` map. Returns `Err` with the
1007/// DynamoDB-style error message for the first undefined reference found.
1008pub fn validate_name_refs(
1009    expr: &ConditionExpr,
1010    attr_names: &Option<HashMap<String, String>>,
1011) -> Result<(), String> {
1012    let mut undefined = Vec::new();
1013    collect_undefined_name_refs(expr, attr_names, &mut undefined);
1014    if let Some(name) = undefined.first() {
1015        Err(format!(
1016            "An expression attribute name used in the document path is not defined; attribute name: {}",
1017            name
1018        ))
1019    } else {
1020        Ok(())
1021    }
1022}
1023
1024fn collect_undefined_name_refs(
1025    expr: &ConditionExpr,
1026    attr_names: &Option<HashMap<String, String>>,
1027    out: &mut Vec<String>,
1028) {
1029    match expr {
1030        ConditionExpr::Comparison { left, right, .. } => {
1031            collect_operand_undefined_refs(left, attr_names, out);
1032            collect_operand_undefined_refs(right, attr_names, out);
1033        }
1034        ConditionExpr::Between { operand, lo, hi } => {
1035            collect_operand_undefined_refs(operand, attr_names, out);
1036            collect_operand_undefined_refs(lo, attr_names, out);
1037            collect_operand_undefined_refs(hi, attr_names, out);
1038        }
1039        ConditionExpr::In { operand, values } => {
1040            collect_operand_undefined_refs(operand, attr_names, out);
1041            for v in values {
1042                collect_operand_undefined_refs(v, attr_names, out);
1043            }
1044        }
1045        ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
1046            collect_path_undefined_refs(path, attr_names, out);
1047        }
1048        ConditionExpr::AttributeType(path, operand) => {
1049            collect_path_undefined_refs(path, attr_names, out);
1050            collect_operand_undefined_refs(operand, attr_names, out);
1051        }
1052        ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
1053            collect_operand_undefined_refs(a, attr_names, out);
1054            collect_operand_undefined_refs(b, attr_names, out);
1055        }
1056        ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
1057            collect_undefined_name_refs(a, attr_names, out);
1058            collect_undefined_name_refs(b, attr_names, out);
1059        }
1060        ConditionExpr::Not(inner) => {
1061            collect_undefined_name_refs(inner, attr_names, out);
1062        }
1063    }
1064}
1065
1066fn collect_operand_undefined_refs(
1067    operand: &Operand,
1068    attr_names: &Option<HashMap<String, String>>,
1069    out: &mut Vec<String>,
1070) {
1071    match operand {
1072        Operand::Path(path) | Operand::Size(path) => {
1073            collect_path_undefined_refs(path, attr_names, out);
1074        }
1075        Operand::ValueRef(_) => {}
1076    }
1077}
1078
1079fn collect_path_undefined_refs(
1080    path: &[PathElement],
1081    attr_names: &Option<HashMap<String, String>>,
1082    out: &mut Vec<String>,
1083) {
1084    for elem in path {
1085        if let PathElement::Attribute(name) = elem {
1086            if name.starts_with('#') {
1087                let defined = attr_names
1088                    .as_ref()
1089                    .is_some_and(|m| m.contains_key(name.as_str()));
1090                if !defined && !out.contains(name) {
1091                    out.push(name.clone());
1092                }
1093            }
1094        }
1095    }
1096}
1097
1098/// Returns true if a DynamoDB number string can be safely compared using f64.
1099/// f64 has 15-17 significant decimal digits of precision; ≤15 digit strings
1100/// are always exactly representable so no precision is lost.
1101fn can_use_f64(s: &str) -> bool {
1102    // Reject scientific notation — uncommon and complicates digit counting
1103    if s.contains('E') || s.contains('e') {
1104        return false;
1105    }
1106    // Count digit characters (skip sign and decimal point).
1107    // If total digits ≤ 15, the number fits exactly in f64.
1108    let digit_count = s.bytes().filter(|b| b.is_ascii_digit()).count();
1109    digit_count <= 15
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114    use super::*;
1115    use crate::expressions::evaluate_without_tracking;
1116
1117    fn make_item(pairs: &[(&str, AttributeValue)]) -> HashMap<String, AttributeValue> {
1118        pairs
1119            .iter()
1120            .map(|(k, v)| (k.to_string(), v.clone()))
1121            .collect()
1122    }
1123
1124    fn vals(pairs: &[(&str, AttributeValue)]) -> Option<HashMap<String, AttributeValue>> {
1125        Some(make_item(pairs))
1126    }
1127
1128    fn names(pairs: &[(&str, &str)]) -> Option<HashMap<String, String>> {
1129        Some(
1130            pairs
1131                .iter()
1132                .map(|(k, v)| (k.to_string(), v.to_string()))
1133                .collect(),
1134        )
1135    }
1136
1137    #[test]
1138    fn test_simple_equality() {
1139        let expr = parse("pk = :val").unwrap();
1140        let item = make_item(&[("pk", AttributeValue::S("hello".into()))]);
1141        let av = vals(&[(":val", AttributeValue::S("hello".into()))]);
1142        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1143    }
1144
1145    #[test]
1146    fn test_inequality() {
1147        let expr = parse("pk <> :val").unwrap();
1148        let item = make_item(&[("pk", AttributeValue::S("hello".into()))]);
1149        let av = vals(&[(":val", AttributeValue::S("world".into()))]);
1150        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1151    }
1152
1153    #[test]
1154    fn test_numeric_comparison() {
1155        let expr = parse("price > :min").unwrap();
1156        let item = make_item(&[("price", AttributeValue::N("42".into()))]);
1157        let av = vals(&[(":min", AttributeValue::N("10".into()))]);
1158        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1159    }
1160
1161    #[test]
1162    fn test_between() {
1163        let expr = parse("age BETWEEN :lo AND :hi").unwrap();
1164        let item = make_item(&[("age", AttributeValue::N("25".into()))]);
1165        let av = vals(&[
1166            (":lo", AttributeValue::N("18".into())),
1167            (":hi", AttributeValue::N("65".into())),
1168        ]);
1169        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1170    }
1171
1172    #[test]
1173    fn test_in_operator() {
1174        let expr = parse("state_val IN (:s1, :s2, :s3)").unwrap();
1175        let item = make_item(&[("state_val", AttributeValue::S("active".into()))]);
1176        let av = vals(&[
1177            (":s1", AttributeValue::S("active".into())),
1178            (":s2", AttributeValue::S("pending".into())),
1179            (":s3", AttributeValue::S("closed".into())),
1180        ]);
1181        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1182    }
1183
1184    #[test]
1185    fn test_attribute_exists() {
1186        let expr = parse("attribute_exists(email)").unwrap();
1187        let item = make_item(&[("email", AttributeValue::S("a@b.com".into()))]);
1188        assert!(evaluate_without_tracking(&expr, &item, &None, &None).unwrap());
1189
1190        let empty_item: HashMap<String, AttributeValue> = HashMap::new();
1191        assert!(!evaluate_without_tracking(&expr, &empty_item, &None, &None).unwrap());
1192    }
1193
1194    #[test]
1195    fn test_attribute_not_exists() {
1196        let expr = parse("attribute_not_exists(email)").unwrap();
1197        let item: HashMap<String, AttributeValue> = HashMap::new();
1198        assert!(evaluate_without_tracking(&expr, &item, &None, &None).unwrap());
1199    }
1200
1201    #[test]
1202    fn test_begins_with() {
1203        let expr = parse("begins_with(sk, :prefix)").unwrap();
1204        let item = make_item(&[("sk", AttributeValue::S("user#123".into()))]);
1205        let av = vals(&[(":prefix", AttributeValue::S("user#".into()))]);
1206        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1207    }
1208
1209    #[test]
1210    fn test_contains_string() {
1211        let expr = parse("contains(description, :sub)").unwrap();
1212        let item = make_item(&[("description", AttributeValue::S("hello world".into()))]);
1213        let av = vals(&[(":sub", AttributeValue::S("world".into()))]);
1214        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1215    }
1216
1217    #[test]
1218    fn test_contains_string_set() {
1219        let expr = parse("contains(tags, :tag)").unwrap();
1220        let item = make_item(&[(
1221            "tags",
1222            AttributeValue::SS(vec!["rust".into(), "dynamo".into()]),
1223        )]);
1224        let av = vals(&[(":tag", AttributeValue::S("rust".into()))]);
1225        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1226    }
1227
1228    #[test]
1229    fn test_size_function() {
1230        let expr = parse("size(label) > :len").unwrap();
1231        let item = make_item(&[("label", AttributeValue::S("Alice".into()))]);
1232        let av = vals(&[(":len", AttributeValue::N("3".into()))]);
1233        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1234    }
1235
1236    #[test]
1237    fn test_and_operator() {
1238        let expr = parse("price > :min AND price < :max").unwrap();
1239        let item = make_item(&[("price", AttributeValue::N("50".into()))]);
1240        let av = vals(&[
1241            (":min", AttributeValue::N("10".into())),
1242            (":max", AttributeValue::N("100".into())),
1243        ]);
1244        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1245    }
1246
1247    #[test]
1248    fn test_or_operator() {
1249        let expr = parse("state_val = :s1 OR state_val = :s2").unwrap();
1250        let item = make_item(&[("state_val", AttributeValue::S("pending".into()))]);
1251        let av = vals(&[
1252            (":s1", AttributeValue::S("active".into())),
1253            (":s2", AttributeValue::S("pending".into())),
1254        ]);
1255        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1256    }
1257
1258    #[test]
1259    fn test_not_operator() {
1260        let expr = parse("NOT state_val = :val").unwrap();
1261        let item = make_item(&[("state_val", AttributeValue::S("active".into()))]);
1262        let av = vals(&[(":val", AttributeValue::S("closed".into()))]);
1263        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1264    }
1265
1266    #[test]
1267    fn test_expression_attribute_names() {
1268        let expr = parse("#s = :val").unwrap();
1269        let item = make_item(&[("status", AttributeValue::S("active".into()))]);
1270        let an = names(&[("#s", "status")]);
1271        let av = vals(&[(":val", AttributeValue::S("active".into()))]);
1272        assert!(evaluate_without_tracking(&expr, &item, &an, &av).unwrap());
1273    }
1274
1275    #[test]
1276    fn test_nested_path() {
1277        let expr = parse("profile.label = :val").unwrap();
1278        let mut nested = HashMap::new();
1279        nested.insert("label".to_string(), AttributeValue::S("Alice".into()));
1280        let item = make_item(&[("profile", AttributeValue::M(nested))]);
1281        let av = vals(&[(":val", AttributeValue::S("Alice".into()))]);
1282        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1283    }
1284
1285    #[test]
1286    fn test_parenthesized() {
1287        let expr = parse("(a = :x OR b = :y) AND c = :z").unwrap();
1288        let item = make_item(&[
1289            ("a", AttributeValue::S("1".into())),
1290            ("b", AttributeValue::S("2".into())),
1291            ("c", AttributeValue::S("3".into())),
1292        ]);
1293        let av = vals(&[
1294            (":x", AttributeValue::S("wrong".into())),
1295            (":y", AttributeValue::S("2".into())),
1296            (":z", AttributeValue::S("3".into())),
1297        ]);
1298        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1299    }
1300
1301    #[test]
1302    fn test_missing_attribute_is_false() {
1303        let expr = parse("nonexistent = :val").unwrap();
1304        let item: HashMap<String, AttributeValue> = HashMap::new();
1305        let av = vals(&[(":val", AttributeValue::S("x".into()))]);
1306        assert!(!evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1307    }
1308}