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