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, check_redundant_parens, 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, PartialEq)]
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    super::check_expression_size(expr)?;
81    let tokens = tokenize(expr).map_err(|e| e.to_string())?;
82    check_redundant_parens(&tokens)?;
83    let mut stream = TokenStream::new(tokens);
84    let result = parse_or(&mut stream)?;
85    if !stream.at_end() {
86        return Err(format!(
87            "Syntax error; token: \"{}\"",
88            stream.peek().unwrap()
89        ));
90    }
91    Ok(result)
92}
93
94/// Evaluate a condition expression against an item, using a `TrackedExpressionAttributes`
95/// to resolve and track which names/values are referenced.
96pub fn evaluate(
97    expr: &ConditionExpr,
98    item: &HashMap<String, AttributeValue>,
99    tracker: &TrackedExpressionAttributes,
100) -> Result<bool, String> {
101    match expr {
102        ConditionExpr::Comparison { left, op, right } => {
103            let lv = resolve_operand(left, item, tracker)?;
104            let rv = resolve_operand(right, item, tracker)?;
105            match (lv, rv) {
106                (Some(l), Some(r)) => Ok(compare_values(&l, op, &r)),
107                // When either operand is missing (attribute doesn't exist):
108                // - <> (not-equals) returns true: a missing value is not equal to anything
109                // - All other comparisons return false: a missing value can't be
110                //   compared for equality, ordering, etc.
111                _ => Ok(matches!(op, CompOp::Ne)),
112            }
113        }
114
115        ConditionExpr::Between { operand, lo, hi } => {
116            let val = resolve_operand(operand, item, tracker)?;
117            let lo_val = resolve_operand(lo, item, tracker)?;
118            let hi_val = resolve_operand(hi, item, tracker)?;
119            match (val, lo_val, hi_val) {
120                (Some(v), Some(l), Some(h)) => {
121                    Ok(compare_values(&v, &CompOp::Ge, &l) && compare_values(&v, &CompOp::Le, &h))
122                }
123                _ => Ok(false),
124            }
125        }
126
127        ConditionExpr::In { operand, values } => {
128            let val = resolve_operand(operand, item, tracker)?;
129            match val {
130                Some(v) => {
131                    for candidate in values {
132                        let cv = resolve_operand(candidate, item, tracker)?;
133                        if let Some(c) = cv {
134                            if compare_values(&v, &CompOp::Eq, &c) {
135                                return Ok(true);
136                            }
137                        }
138                    }
139                    Ok(false)
140                }
141                None => Ok(false),
142            }
143        }
144
145        ConditionExpr::AttributeExists(path) => {
146            let resolved = resolve_path_elements(path, tracker)?;
147            Ok(resolve_path(item, &resolved).is_some())
148        }
149
150        ConditionExpr::AttributeNotExists(path) => {
151            let resolved = resolve_path_elements(path, tracker)?;
152            Ok(resolve_path(item, &resolved).is_none())
153        }
154
155        ConditionExpr::AttributeType(path, type_operand) => {
156            let resolved = resolve_path_elements(path, tracker)?;
157            let val = resolve_path(item, &resolved);
158            let type_val = resolve_operand(type_operand, item, tracker)?;
159            match (val, type_val) {
160                (Some(v), Some(AttributeValue::S(type_name))) => Ok(v.type_name() == type_name),
161                _ => Ok(false),
162            }
163        }
164
165        ConditionExpr::BeginsWith(path_op, prefix_op) => {
166            let val = resolve_operand(path_op, item, tracker)?;
167            let prefix = resolve_operand(prefix_op, item, tracker)?;
168            match (val, prefix) {
169                (Some(AttributeValue::S(s)), Some(AttributeValue::S(p))) => Ok(s.starts_with(&p)),
170                (Some(AttributeValue::B(b)), Some(AttributeValue::B(p))) => Ok(b.starts_with(&p)),
171                _ => Ok(false),
172            }
173        }
174
175        ConditionExpr::Contains(path_op, search_op) => {
176            let val = resolve_operand(path_op, item, tracker)?;
177            let search = resolve_operand(search_op, item, tracker)?;
178            match (val, search) {
179                (Some(AttributeValue::S(s)), Some(AttributeValue::S(sub))) => Ok(s.contains(&sub)),
180                (Some(AttributeValue::B(b)), Some(AttributeValue::B(sub))) => {
181                    Ok(sub.is_empty() || b.windows(sub.len()).any(|w| w == sub.as_slice()))
182                }
183                (Some(AttributeValue::SS(set)), Some(AttributeValue::S(elem))) => {
184                    Ok(set.contains(&elem))
185                }
186                (Some(AttributeValue::NS(set)), Some(AttributeValue::N(elem))) => {
187                    Ok(set.contains(&elem))
188                }
189                (Some(AttributeValue::BS(set)), Some(AttributeValue::B(elem))) => {
190                    Ok(set.contains(&elem))
191                }
192                (Some(AttributeValue::L(list)), Some(search_val)) => Ok(list
193                    .iter()
194                    .any(|v| compare_values(v, &CompOp::Eq, &search_val))),
195                _ => Ok(false),
196            }
197        }
198
199        ConditionExpr::And(left, right) => {
200            if !evaluate(left, item, tracker)? {
201                return Ok(false); // short-circuit
202            }
203            evaluate(right, item, tracker)
204        }
205
206        ConditionExpr::Or(left, right) => {
207            if evaluate(left, item, tracker)? {
208                return Ok(true); // short-circuit
209            }
210            evaluate(right, item, tracker)
211        }
212
213        ConditionExpr::Not(inner) => {
214            let v = evaluate(inner, item, tracker)?;
215            Ok(!v)
216        }
217    }
218}
219
220// ---------------------------------------------------------------------------
221// Parser (recursive descent with precedence: OR < AND < NOT < comparison)
222// ---------------------------------------------------------------------------
223
224fn parse_or(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
225    let mut left = parse_and(stream)?;
226    while matches!(stream.peek(), Some(Token::Or)) {
227        stream.next();
228        let right = parse_and(stream)?;
229        left = ConditionExpr::Or(Box::new(left), Box::new(right));
230    }
231    Ok(left)
232}
233
234fn parse_and(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
235    let mut left = parse_not(stream)?;
236    while matches!(stream.peek(), Some(Token::And)) {
237        stream.next();
238        let right = parse_not(stream)?;
239        left = ConditionExpr::And(Box::new(left), Box::new(right));
240    }
241    Ok(left)
242}
243
244fn parse_not(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
245    if matches!(stream.peek(), Some(Token::Not)) {
246        stream.next();
247        let inner = parse_not(stream)?;
248        return Ok(ConditionExpr::Not(Box::new(inner)));
249    }
250    parse_primary(stream)
251}
252
253fn parse_primary(stream: &mut TokenStream) -> Result<ConditionExpr, String> {
254    // Parenthesized expression
255    if matches!(stream.peek(), Some(Token::LParen)) {
256        stream.next();
257        let expr = parse_or(stream)?;
258        stream.expect(&Token::RParen)?;
259        return Ok(expr);
260    }
261
262    // Check for function calls
263    if let Some(Token::Identifier(name)) = stream.peek() {
264        let name_owned = name.clone();
265        let func_name = name_owned.to_lowercase();
266
267        // Check if next token after identifier is '(' — if so, it's a function call
268        let is_function_call = {
269            let saved = stream.pos();
270            stream.next(); // consume identifier
271            let is_lparen = matches!(stream.peek(), Some(Token::LParen));
272            stream.set_pos(saved); // restore position
273            is_lparen
274        };
275
276        if is_function_call {
277            // Known condition-level functions (return bool, valid as primary expression)
278            match func_name.as_str() {
279                "attribute_exists" => {
280                    stream.next();
281                    stream.expect(&Token::LParen)?;
282                    let path = parse_raw_path(stream)?;
283                    stream.expect(&Token::RParen)?;
284                    return Ok(ConditionExpr::AttributeExists(path));
285                }
286                "attribute_not_exists" => {
287                    stream.next();
288                    stream.expect(&Token::LParen)?;
289                    let path = parse_raw_path(stream)?;
290                    stream.expect(&Token::RParen)?;
291                    return Ok(ConditionExpr::AttributeNotExists(path));
292                }
293                "attribute_type" => {
294                    stream.next();
295                    stream.expect(&Token::LParen)?;
296                    let path = parse_raw_path(stream)?;
297                    stream.expect(&Token::Comma)?;
298                    let type_val = parse_operand(stream)?;
299                    stream.expect(&Token::RParen)?;
300                    return Ok(ConditionExpr::AttributeType(path, type_val));
301                }
302                "begins_with" => {
303                    stream.next();
304                    stream.expect(&Token::LParen)?;
305                    let path_op = parse_operand(stream)?;
306                    stream.expect(&Token::Comma)?;
307                    let prefix_op = parse_operand(stream)?;
308                    stream.expect(&Token::RParen)?;
309                    return Ok(ConditionExpr::BeginsWith(path_op, prefix_op));
310                }
311                "contains" => {
312                    stream.next();
313                    stream.expect(&Token::LParen)?;
314                    let path_op = parse_operand(stream)?;
315                    stream.expect(&Token::Comma)?;
316                    let search_op = parse_operand(stream)?;
317                    stream.expect(&Token::RParen)?;
318                    return Ok(ConditionExpr::Contains(path_op, search_op));
319                }
320                "size" => {
321                    // size() is an operand-level function, not a condition-level function.
322                    // Fall through to comparison parsing where parse_operand handles it.
323                }
324                _ => {
325                    // Unknown function name
326                    return Err(format!("Invalid function name; function: {}", name_owned));
327                }
328            }
329        } else {
330            // Not a function call — fall through to comparison parsing.
331            // Reserved keyword check happens in parse_raw_path.
332        }
333    }
334
335    // Comparison: operand op operand, or operand BETWEEN, or operand IN
336    let left = parse_operand(stream)?;
337
338    match stream.peek() {
339        Some(Token::Eq) => {
340            stream.next();
341            let right = parse_operand(stream)?;
342            Ok(ConditionExpr::Comparison {
343                left,
344                op: CompOp::Eq,
345                right,
346            })
347        }
348        Some(Token::Ne) => {
349            stream.next();
350            let right = parse_operand(stream)?;
351            Ok(ConditionExpr::Comparison {
352                left,
353                op: CompOp::Ne,
354                right,
355            })
356        }
357        Some(Token::Lt) => {
358            stream.next();
359            let right = parse_operand(stream)?;
360            Ok(ConditionExpr::Comparison {
361                left,
362                op: CompOp::Lt,
363                right,
364            })
365        }
366        Some(Token::Le) => {
367            stream.next();
368            let right = parse_operand(stream)?;
369            Ok(ConditionExpr::Comparison {
370                left,
371                op: CompOp::Le,
372                right,
373            })
374        }
375        Some(Token::Gt) => {
376            stream.next();
377            let right = parse_operand(stream)?;
378            Ok(ConditionExpr::Comparison {
379                left,
380                op: CompOp::Gt,
381                right,
382            })
383        }
384        Some(Token::Ge) => {
385            stream.next();
386            let right = parse_operand(stream)?;
387            Ok(ConditionExpr::Comparison {
388                left,
389                op: CompOp::Ge,
390                right,
391            })
392        }
393        Some(Token::Between) => {
394            stream.next();
395            let lo = parse_operand(stream)?;
396            stream.expect(&Token::And)?;
397            let hi = parse_operand(stream)?;
398            Ok(ConditionExpr::Between {
399                operand: left,
400                lo,
401                hi,
402            })
403        }
404        Some(Token::In) => {
405            stream.next();
406            stream.expect(&Token::LParen)?;
407            let mut values = vec![parse_operand(stream)?];
408            while matches!(stream.peek(), Some(Token::Comma)) {
409                stream.next();
410                values.push(parse_operand(stream)?);
411            }
412            stream.expect(&Token::RParen)?;
413            Ok(ConditionExpr::In {
414                operand: left,
415                values,
416            })
417        }
418        _ => Err("Expected comparison operator, BETWEEN, or IN".to_string()),
419    }
420}
421
422/// Parse an operand (path, value ref, or size function).
423fn parse_operand(stream: &mut TokenStream) -> Result<Operand, String> {
424    // Check for size() function
425    if let Some(Token::Identifier(name)) = stream.peek() {
426        if name.to_lowercase() == "size" {
427            stream.next();
428            stream.expect(&Token::LParen)?;
429            let path = parse_raw_path(stream)?;
430            stream.expect(&Token::RParen)?;
431            return Ok(Operand::Size(path));
432        }
433    }
434
435    match stream.peek() {
436        Some(Token::ValueRef(_)) => {
437            if let Some(Token::ValueRef(name)) = stream.next().cloned() {
438                Ok(Operand::ValueRef(name))
439            } else {
440                unreachable!()
441            }
442        }
443        Some(Token::Identifier(_)) | Some(Token::NameRef(_)) => {
444            let path = parse_raw_path(stream)?;
445            Ok(Operand::Path(path))
446        }
447        Some(t) => Err(format!("Expected operand, got {t}")),
448        None => Err("Expected operand, got end of expression".to_string()),
449    }
450}
451
452/// Parse a raw document path (not resolving #names yet).
453/// Path format: `ident(.ident | [n])*`
454pub fn parse_raw_path(stream: &mut TokenStream) -> Result<Vec<PathElement>, String> {
455    let first = match stream.next() {
456        Some(Token::Identifier(name)) => {
457            if super::reserved::is_reserved_keyword(name) {
458                return Err(format!(
459                    "Attribute name is a reserved keyword; reserved keyword: {name}"
460                ));
461            }
462            PathElement::Attribute(name.clone())
463        }
464        Some(Token::NameRef(name)) => PathElement::Attribute(name.clone()),
465        Some(t) => return Err(format!("Expected attribute name, got {t}")),
466        None => return Err("Expected attribute name, got end of expression".to_string()),
467    };
468
469    let mut path = vec![first];
470
471    loop {
472        match stream.peek() {
473            Some(Token::Dot) => {
474                stream.next();
475                match stream.next() {
476                    Some(Token::Identifier(name)) => {
477                        if super::reserved::is_reserved_keyword(name) {
478                            return Err(format!(
479                                "Attribute name is a reserved keyword; reserved keyword: {name}"
480                            ));
481                        }
482                        path.push(PathElement::Attribute(name.clone()));
483                    }
484                    Some(Token::NameRef(name)) => {
485                        path.push(PathElement::Attribute(name.clone()));
486                    }
487                    Some(t) => return Err(format!("Expected attribute name after '.', got {t}")),
488                    None => return Err("Expected attribute name after '.'".to_string()),
489                }
490            }
491            Some(Token::LBracket) => {
492                stream.next();
493                match stream.next() {
494                    Some(Token::Number(n)) => {
495                        let idx: usize = n.parse().map_err(|_| format!("Invalid index: {n}"))?;
496                        path.push(PathElement::Index(idx));
497                    }
498                    Some(t) => return Err(format!("Expected number in brackets, got {t}")),
499                    None => return Err("Expected number in brackets".to_string()),
500                }
501                stream.expect(&Token::RBracket)?;
502            }
503            _ => break,
504        }
505    }
506
507    Ok(path)
508}
509
510// ---------------------------------------------------------------------------
511// Value resolution
512// ---------------------------------------------------------------------------
513
514/// Resolve an operand to an AttributeValue, tracking usage.
515fn resolve_operand(
516    operand: &Operand,
517    item: &HashMap<String, AttributeValue>,
518    tracker: &TrackedExpressionAttributes,
519) -> Result<Option<AttributeValue>, String> {
520    match operand {
521        Operand::Path(path) => {
522            let resolved = resolve_path_elements(path, tracker)?;
523            Ok(resolve_path(item, &resolved))
524        }
525        Operand::ValueRef(name) => {
526            let val = tracker.resolve_value(name)?;
527            Ok(Some(val.clone()))
528        }
529        Operand::Size(path) => {
530            let resolved = resolve_path_elements(path, tracker)?;
531            match resolve_path(item, &resolved) {
532                Some(val) => {
533                    let size = match &val {
534                        // DynamoDB measures string size in UTF-16 code units,
535                        // not UTF-8 bytes (so a surrogate pair counts as 2).
536                        AttributeValue::S(s) => s.encode_utf16().count(),
537                        AttributeValue::B(b) => b.len(),
538                        AttributeValue::SS(set) => set.len(),
539                        AttributeValue::NS(set) => set.len(),
540                        AttributeValue::BS(set) => set.len(),
541                        AttributeValue::L(list) => list.len(),
542                        AttributeValue::M(map) => map.len(),
543                        // N, BOOL, NULL do not support size() — return None
544                        // so the comparison evaluates to false (no match).
545                        _ => return Ok(None),
546                    };
547                    Ok(Some(AttributeValue::N(size.to_string())))
548                }
549                None => Ok(None),
550            }
551        }
552    }
553}
554
555// ---------------------------------------------------------------------------
556// Comparison logic
557// ---------------------------------------------------------------------------
558
559/// Compare two AttributeValues using a comparison operator.
560fn compare_values(left: &AttributeValue, op: &CompOp, right: &AttributeValue) -> bool {
561    match (left, right) {
562        // String comparisons
563        (AttributeValue::S(a), AttributeValue::S(b)) => compare_ord(a, b, op),
564
565        // Number comparisons — f64 fast-path for common cases, BigDecimal for edge cases
566        (AttributeValue::N(a), AttributeValue::N(b)) => {
567            // Fast path: f64 is exact for ≤15 significant digits with no scientific notation
568            if can_use_f64(a) && can_use_f64(b) {
569                if let (Ok(fa), Ok(fb)) = (a.parse::<f64>(), b.parse::<f64>()) {
570                    if fa.is_finite() && fb.is_finite() {
571                        return compare_ord(&fa, &fb, op);
572                    }
573                }
574            }
575            // Slow path: BigDecimal for 38-digit precision edge cases
576            use bigdecimal::BigDecimal;
577            use std::str::FromStr;
578            match (BigDecimal::from_str(a), BigDecimal::from_str(b)) {
579                (Ok(da), Ok(db)) => compare_ord(&da, &db, op),
580                _ => false,
581            }
582        }
583
584        // Binary comparisons
585        (AttributeValue::B(a), AttributeValue::B(b)) => compare_ord(a, b, op),
586
587        // Bool — only equality
588        (AttributeValue::BOOL(a), AttributeValue::BOOL(b)) => match op {
589            CompOp::Eq => a == b,
590            CompOp::Ne => a != b,
591            _ => false,
592        },
593
594        // Null — only equality
595        (AttributeValue::NULL(a), AttributeValue::NULL(b)) => match op {
596            CompOp::Eq => a == b,
597            CompOp::Ne => a != b,
598            _ => false,
599        },
600
601        // String Set — set equality (order-independent)
602        (AttributeValue::SS(a), AttributeValue::SS(b)) => {
603            let mut sa = a.clone();
604            let mut sb = b.clone();
605            sa.sort();
606            sb.sort();
607            match op {
608                CompOp::Eq => sa == sb,
609                CompOp::Ne => sa != sb,
610                _ => false,
611            }
612        }
613
614        // Number Set — set equality (order-independent). Compared via the canonical
615        // numeric form (full 38-digit precision), matching how NS duplicates are
616        // detected on write. f64 would collapse values differing only beyond ~15
617        // significant digits and wrongly report distinct sets as equal.
618        (AttributeValue::NS(a), AttributeValue::NS(b)) => {
619            if a.len() != b.len() {
620                return matches!(op, CompOp::Ne);
621            }
622            let mut na: Vec<String> = a
623                .iter()
624                .map(|n| crate::types::normalize_dynamo_number(n))
625                .collect();
626            let mut nb: Vec<String> = b
627                .iter()
628                .map(|n| crate::types::normalize_dynamo_number(n))
629                .collect();
630            na.sort();
631            nb.sort();
632            match op {
633                CompOp::Eq => na == nb,
634                CompOp::Ne => na != nb,
635                _ => false,
636            }
637        }
638
639        // Binary Set — set equality (order-independent)
640        (AttributeValue::BS(a), AttributeValue::BS(b)) => {
641            let mut sa = a.clone();
642            let mut sb = b.clone();
643            sa.sort();
644            sb.sort();
645            match op {
646                CompOp::Eq => sa == sb,
647                CompOp::Ne => sa != sb,
648                _ => false,
649            }
650        }
651
652        // List: element-wise deep equality, order-sensitive. Recurses so nested
653        // numbers and documents use the same comparison semantics as scalars.
654        // DynamoDB only allows = and <> on documents, not ordering.
655        (AttributeValue::L(a), AttributeValue::L(b)) => {
656            let equal = a.len() == b.len()
657                && a.iter()
658                    .zip(b.iter())
659                    .all(|(x, y)| compare_values(x, &CompOp::Eq, y));
660            match op {
661                CompOp::Eq => equal,
662                CompOp::Ne => !equal,
663                _ => false,
664            }
665        }
666
667        // Map: key-set plus per-key deep equality, order-independent. Recurses so
668        // nested numbers normalise (1 == 1.0) and nested documents compare deeply.
669        (AttributeValue::M(a), AttributeValue::M(b)) => {
670            let equal = a.len() == b.len()
671                && a.iter().all(|(k, av)| {
672                    b.get(k)
673                        .is_some_and(|bv| compare_values(av, &CompOp::Eq, bv))
674                });
675            match op {
676                CompOp::Eq => equal,
677                CompOp::Ne => !equal,
678                _ => false,
679            }
680        }
681
682        // Different types — only <> is true
683        _ => matches!(op, CompOp::Ne),
684    }
685}
686
687fn compare_ord<T: PartialOrd>(a: &T, b: &T, op: &CompOp) -> bool {
688    match op {
689        CompOp::Eq => a == b,
690        CompOp::Ne => a != b,
691        CompOp::Lt => a < b,
692        CompOp::Le => a <= b,
693        CompOp::Gt => a > b,
694        CompOp::Ge => a >= b,
695    }
696}
697
698/// Walk a condition expression and track all attribute name and value references
699/// without evaluating. Used for pre-validation to detect unused names/values.
700pub fn track_references(
701    expr: &ConditionExpr,
702    tracker: &TrackedExpressionAttributes,
703) -> Result<(), String> {
704    match expr {
705        ConditionExpr::Comparison { left, op: _, right } => {
706            track_operand_refs(left, tracker)?;
707            track_operand_refs(right, tracker)
708        }
709        ConditionExpr::Between { operand, lo, hi } => {
710            track_operand_refs(operand, tracker)?;
711            track_operand_refs(lo, tracker)?;
712            track_operand_refs(hi, tracker)
713        }
714        ConditionExpr::In { operand, values } => {
715            track_operand_refs(operand, tracker)?;
716            for v in values {
717                track_operand_refs(v, tracker)?;
718            }
719            Ok(())
720        }
721        ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
722            track_cond_path_refs(path, tracker)
723        }
724        ConditionExpr::AttributeType(path, type_op) => {
725            track_cond_path_refs(path, tracker)?;
726            track_operand_refs(type_op, tracker)
727        }
728        ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
729            track_operand_refs(a, tracker)?;
730            track_operand_refs(b, tracker)
731        }
732        ConditionExpr::And(left, right) | ConditionExpr::Or(left, right) => {
733            track_references(left, tracker)?;
734            track_references(right, tracker)
735        }
736        ConditionExpr::Not(inner) => track_references(inner, tracker),
737    }
738}
739
740fn track_operand_refs(
741    operand: &Operand,
742    tracker: &TrackedExpressionAttributes,
743) -> Result<(), String> {
744    match operand {
745        Operand::Path(path) => track_cond_path_refs(path, tracker),
746        Operand::ValueRef(name) => {
747            tracker.resolve_value(name)?;
748            Ok(())
749        }
750        Operand::Size(path) => track_cond_path_refs(path, tracker),
751    }
752}
753
754fn track_cond_path_refs(
755    path: &[PathElement],
756    tracker: &TrackedExpressionAttributes,
757) -> Result<(), String> {
758    for elem in path {
759        if let PathElement::Attribute(name) = elem {
760            if name.starts_with('#') {
761                tracker.resolve_name(name)?;
762            }
763        }
764    }
765    Ok(())
766}
767
768/// Statically validate a condition expression against ExpressionAttributeValues.
769///
770/// Checks BETWEEN operands for:
771/// - Same data type (lower and upper bound must have the same type)
772/// - Correct ordering (lower bound must not be greater than upper bound)
773///
774/// This validation happens before table lookup, matching DynamoDB behaviour.
775/// Semantic validation that needs resolved names and/or values: rejects
776/// `contains(x, x)` (operands must be distinct) and `begins_with(_, :v)` where
777/// the value operand is not a string or binary. Returns the raw reason; callers
778/// add the `Invalid <X>Expression:` prefix. Runs before execution, so it
779/// rejects on empty tables the way real DynamoDB does.
780pub fn validate_operand_semantics(
781    expr: &ConditionExpr,
782    names: &Option<HashMap<String, String>>,
783    values: &Option<HashMap<String, AttributeValue>>,
784) -> Result<(), String> {
785    match expr {
786        ConditionExpr::Contains(first, rest) => {
787            if first == rest {
788                return Err(format!(
789                    "The first operand must be distinct from the remaining operands for this operator or function; operator: contains, first operand: [{}]",
790                    render_operand_name(first, names)
791                ));
792            }
793            Ok(())
794        }
795        ConditionExpr::BeginsWith(_, prefix) => {
796            if let Operand::ValueRef(vname) = prefix {
797                if let Some(v) = values.as_ref().and_then(|m| m.get(vname.as_str())) {
798                    if !matches!(v, AttributeValue::S(_) | AttributeValue::B(_)) {
799                        return Err(format!(
800                            "Incorrect operand type for operator or function; operator or function: begins_with, operand type: {}",
801                            v.type_name()
802                        ));
803                    }
804                }
805            }
806            Ok(())
807        }
808        ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
809            validate_operand_semantics(a, names, values)?;
810            validate_operand_semantics(b, names, values)
811        }
812        ConditionExpr::Not(inner) => validate_operand_semantics(inner, names, values),
813        _ => Ok(()),
814    }
815}
816
817/// Render an operand's path for error messages, resolving `#name` aliases.
818fn render_operand_name(op: &Operand, names: &Option<HashMap<String, String>>) -> String {
819    let elems = match op {
820        Operand::Path(elems) | Operand::Size(elems) => elems,
821        Operand::ValueRef(name) => return name.clone(),
822    };
823    elems
824        .iter()
825        .map(|e| match e {
826            PathElement::Attribute(a) if a.starts_with('#') => names
827                .as_ref()
828                .and_then(|m| m.get(a))
829                .cloned()
830                .unwrap_or_else(|| a.clone()),
831            PathElement::Attribute(a) => a.clone(),
832            PathElement::Index(i) => format!("[{i}]"),
833        })
834        .collect::<Vec<_>>()
835        .join(".")
836}
837
838pub fn validate_static(
839    expr: &ConditionExpr,
840    values: &Option<HashMap<String, AttributeValue>>,
841) -> Result<(), String> {
842    match expr {
843        ConditionExpr::Between { operand: _, lo, hi } => {
844            // Only validate when both bounds are value refs
845            if let (Operand::ValueRef(lo_name), Operand::ValueRef(hi_name)) = (lo, hi) {
846                if let Some(vals) = values {
847                    let lo_val = vals.get(lo_name.as_str());
848                    let hi_val = vals.get(hi_name.as_str());
849                    if let (Some(lo_v), Some(hi_v)) = (lo_val, hi_val) {
850                        // Check same data type
851                        if std::mem::discriminant(lo_v) != std::mem::discriminant(hi_v) {
852                            return Err(format!(
853                                "Invalid ConditionExpression: The BETWEEN operator requires same data type for lower and upper bounds; \
854                                 lower bound operand: AttributeValue: {{{}}}, upper bound operand: AttributeValue: {{{}}}",
855                                format_av_for_error(lo_v),
856                                format_av_for_error(hi_v),
857                            ));
858                        }
859                        // Check ordering
860                        if compare_values(lo_v, &CompOp::Gt, hi_v) {
861                            return Err(format!(
862                                "Invalid ConditionExpression: The BETWEEN operator requires upper bound to be greater than or equal to lower bound; \
863                                 lower bound operand: AttributeValue: {{{}}}, upper bound operand: AttributeValue: {{{}}}",
864                                format_av_for_error(lo_v),
865                                format_av_for_error(hi_v),
866                            ));
867                        }
868                    }
869                }
870            }
871            Ok(())
872        }
873        ConditionExpr::And(left, right) | ConditionExpr::Or(left, right) => {
874            validate_static(left, values)?;
875            validate_static(right, values)
876        }
877        ConditionExpr::Not(inner) => validate_static(inner, values),
878        _ => Ok(()),
879    }
880}
881
882/// Format an AttributeValue for error messages (e.g., "S:hello", "N:42").
883fn format_av_for_error(av: &AttributeValue) -> String {
884    match av {
885        AttributeValue::S(s) => format!("S:{s}"),
886        AttributeValue::N(n) => format!("N:{n}"),
887        AttributeValue::B(b) => {
888            use base64::Engine;
889            format!("B:{}", base64::engine::general_purpose::STANDARD.encode(b))
890        }
891        AttributeValue::BOOL(b) => format!("BOOL:{b}"),
892        AttributeValue::NULL(_) => "NULL:true".to_string(),
893        AttributeValue::SS(set) => format!("SS:{set:?}"),
894        AttributeValue::NS(set) => format!("NS:{set:?}"),
895        AttributeValue::BS(_) => "BS:[...]".to_string(),
896        AttributeValue::L(_) => "L:[...]".to_string(),
897        AttributeValue::M(_) => "M:{...}".to_string(),
898    }
899}
900
901/// Check for non-scalar key access in an expression.
902///
903/// DynamoDB rejects expressions that use `.` (map lookup) or `[]` (list index) on
904/// key attributes. Returns the offending key attribute name if found.
905///
906/// `key_attrs` contains the effective key attribute names.
907/// `index_key_attrs` contains secondary index key attribute names (for "IndexKey:" prefix).
908pub fn check_non_scalar_key_access(
909    expr: &ConditionExpr,
910    attr_names: &Option<HashMap<String, String>>,
911    key_attrs: &[String],
912    index_key_attrs: &[String],
913) -> Option<(String, bool)> {
914    // Returns (attr_name, is_index_key)
915    let mut result = None;
916    check_non_scalar_key_access_inner(expr, attr_names, key_attrs, index_key_attrs, &mut result);
917    result
918}
919
920fn check_non_scalar_key_access_inner(
921    expr: &ConditionExpr,
922    attr_names: &Option<HashMap<String, String>>,
923    key_attrs: &[String],
924    index_key_attrs: &[String],
925    result: &mut Option<(String, bool)>,
926) {
927    if result.is_some() {
928        return;
929    }
930    match expr {
931        ConditionExpr::Comparison { left, right, .. } => {
932            check_operand_non_scalar(left, attr_names, key_attrs, index_key_attrs, result);
933            check_operand_non_scalar(right, attr_names, key_attrs, index_key_attrs, result);
934        }
935        ConditionExpr::Between { operand, lo, hi } => {
936            check_operand_non_scalar(operand, attr_names, key_attrs, index_key_attrs, result);
937            check_operand_non_scalar(lo, attr_names, key_attrs, index_key_attrs, result);
938            check_operand_non_scalar(hi, attr_names, key_attrs, index_key_attrs, result);
939        }
940        ConditionExpr::In { operand, values } => {
941            check_operand_non_scalar(operand, attr_names, key_attrs, index_key_attrs, result);
942            for v in values {
943                check_operand_non_scalar(v, attr_names, key_attrs, index_key_attrs, result);
944            }
945        }
946        ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
947            check_path_non_scalar(path, attr_names, key_attrs, index_key_attrs, result);
948        }
949        ConditionExpr::AttributeType(path, _) => {
950            check_path_non_scalar(path, attr_names, key_attrs, index_key_attrs, result);
951        }
952        ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
953            check_operand_non_scalar(a, attr_names, key_attrs, index_key_attrs, result);
954            check_operand_non_scalar(b, attr_names, key_attrs, index_key_attrs, result);
955        }
956        ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
957            check_non_scalar_key_access_inner(a, attr_names, key_attrs, index_key_attrs, result);
958            check_non_scalar_key_access_inner(b, attr_names, key_attrs, index_key_attrs, result);
959        }
960        ConditionExpr::Not(inner) => {
961            check_non_scalar_key_access_inner(
962                inner,
963                attr_names,
964                key_attrs,
965                index_key_attrs,
966                result,
967            );
968        }
969    }
970}
971
972fn check_operand_non_scalar(
973    operand: &Operand,
974    attr_names: &Option<HashMap<String, String>>,
975    key_attrs: &[String],
976    index_key_attrs: &[String],
977    result: &mut Option<(String, bool)>,
978) {
979    if result.is_some() {
980        return;
981    }
982    match operand {
983        Operand::Path(path) | Operand::Size(path) => {
984            check_path_non_scalar(path, attr_names, key_attrs, index_key_attrs, result);
985        }
986        Operand::ValueRef(_) => {}
987    }
988}
989
990fn check_path_non_scalar(
991    path: &[PathElement],
992    attr_names: &Option<HashMap<String, String>>,
993    key_attrs: &[String],
994    index_key_attrs: &[String],
995    result: &mut Option<(String, bool)>,
996) {
997    if result.is_some() || path.len() <= 1 {
998        return; // single-element paths are fine (scalar access)
999    }
1000    if let Some(name) = resolve_top_level_path(path, attr_names) {
1001        if key_attrs.contains(&name) {
1002            *result = Some((name, false));
1003        } else if index_key_attrs.contains(&name) {
1004            *result = Some((name, true));
1005        }
1006    }
1007}
1008
1009/// Extract the top-level attribute names referenced in a condition expression.
1010///
1011/// Resolves `#name` references using `expression_attribute_names`.
1012/// For paths like `a.b.c` or `a[1]`, only the root attribute `a` is returned.
1013/// This is used for checking that FilterExpression doesn't reference key attributes.
1014pub fn extract_top_level_attributes(
1015    expr: &ConditionExpr,
1016    attr_names: &Option<HashMap<String, String>>,
1017) -> Vec<String> {
1018    let mut attrs = Vec::new();
1019    collect_top_level_attrs(expr, attr_names, &mut attrs);
1020    attrs.sort();
1021    attrs.dedup();
1022    attrs
1023}
1024
1025fn collect_top_level_attrs(
1026    expr: &ConditionExpr,
1027    attr_names: &Option<HashMap<String, String>>,
1028    out: &mut Vec<String>,
1029) {
1030    match expr {
1031        ConditionExpr::Comparison { left, right, .. } => {
1032            collect_operand_top_attr(left, attr_names, out);
1033            collect_operand_top_attr(right, attr_names, out);
1034        }
1035        ConditionExpr::Between { operand, lo, hi } => {
1036            collect_operand_top_attr(operand, attr_names, out);
1037            collect_operand_top_attr(lo, attr_names, out);
1038            collect_operand_top_attr(hi, attr_names, out);
1039        }
1040        ConditionExpr::In { operand, values } => {
1041            collect_operand_top_attr(operand, attr_names, out);
1042            for v in values {
1043                collect_operand_top_attr(v, attr_names, out);
1044            }
1045        }
1046        ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
1047            if let Some(name) = resolve_top_level_path(path, attr_names) {
1048                out.push(name);
1049            }
1050        }
1051        ConditionExpr::AttributeType(path, _) => {
1052            if let Some(name) = resolve_top_level_path(path, attr_names) {
1053                out.push(name);
1054            }
1055        }
1056        ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
1057            collect_operand_top_attr(a, attr_names, out);
1058            collect_operand_top_attr(b, attr_names, out);
1059        }
1060        ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
1061            collect_top_level_attrs(a, attr_names, out);
1062            collect_top_level_attrs(b, attr_names, out);
1063        }
1064        ConditionExpr::Not(inner) => {
1065            collect_top_level_attrs(inner, attr_names, out);
1066        }
1067    }
1068}
1069
1070fn collect_operand_top_attr(
1071    operand: &Operand,
1072    attr_names: &Option<HashMap<String, String>>,
1073    out: &mut Vec<String>,
1074) {
1075    match operand {
1076        Operand::Path(path) => {
1077            if let Some(name) = resolve_top_level_path(path, attr_names) {
1078                out.push(name);
1079            }
1080        }
1081        Operand::Size(path) => {
1082            if let Some(name) = resolve_top_level_path(path, attr_names) {
1083                out.push(name);
1084            }
1085        }
1086        Operand::ValueRef(_) => {}
1087    }
1088}
1089
1090fn resolve_top_level_path(
1091    path: &[PathElement],
1092    attr_names: &Option<HashMap<String, String>>,
1093) -> Option<String> {
1094    match path.first() {
1095        Some(PathElement::Attribute(name)) => {
1096            if name.starts_with('#') {
1097                attr_names
1098                    .as_ref()
1099                    .and_then(|m| m.get(name.as_str()))
1100                    .cloned()
1101            } else {
1102                Some(name.clone())
1103            }
1104        }
1105        _ => None,
1106    }
1107}
1108
1109/// Validate that all `#name` references in a condition expression are defined
1110/// in the provided `ExpressionAttributeNames` map. Returns `Err` with the
1111/// DynamoDB-style error message for the first undefined reference found.
1112pub fn validate_name_refs(
1113    expr: &ConditionExpr,
1114    attr_names: &Option<HashMap<String, String>>,
1115) -> Result<(), String> {
1116    let mut undefined = Vec::new();
1117    collect_undefined_name_refs(expr, attr_names, &mut undefined);
1118    if let Some(name) = undefined.first() {
1119        Err(format!(
1120            "An expression attribute name used in the document path is not defined; attribute name: {}",
1121            name
1122        ))
1123    } else {
1124        Ok(())
1125    }
1126}
1127
1128fn collect_undefined_name_refs(
1129    expr: &ConditionExpr,
1130    attr_names: &Option<HashMap<String, String>>,
1131    out: &mut Vec<String>,
1132) {
1133    match expr {
1134        ConditionExpr::Comparison { left, right, .. } => {
1135            collect_operand_undefined_refs(left, attr_names, out);
1136            collect_operand_undefined_refs(right, attr_names, out);
1137        }
1138        ConditionExpr::Between { operand, lo, hi } => {
1139            collect_operand_undefined_refs(operand, attr_names, out);
1140            collect_operand_undefined_refs(lo, attr_names, out);
1141            collect_operand_undefined_refs(hi, attr_names, out);
1142        }
1143        ConditionExpr::In { operand, values } => {
1144            collect_operand_undefined_refs(operand, attr_names, out);
1145            for v in values {
1146                collect_operand_undefined_refs(v, attr_names, out);
1147            }
1148        }
1149        ConditionExpr::AttributeExists(path) | ConditionExpr::AttributeNotExists(path) => {
1150            collect_path_undefined_refs(path, attr_names, out);
1151        }
1152        ConditionExpr::AttributeType(path, operand) => {
1153            collect_path_undefined_refs(path, attr_names, out);
1154            collect_operand_undefined_refs(operand, attr_names, out);
1155        }
1156        ConditionExpr::BeginsWith(a, b) | ConditionExpr::Contains(a, b) => {
1157            collect_operand_undefined_refs(a, attr_names, out);
1158            collect_operand_undefined_refs(b, attr_names, out);
1159        }
1160        ConditionExpr::And(a, b) | ConditionExpr::Or(a, b) => {
1161            collect_undefined_name_refs(a, attr_names, out);
1162            collect_undefined_name_refs(b, attr_names, out);
1163        }
1164        ConditionExpr::Not(inner) => {
1165            collect_undefined_name_refs(inner, attr_names, out);
1166        }
1167    }
1168}
1169
1170fn collect_operand_undefined_refs(
1171    operand: &Operand,
1172    attr_names: &Option<HashMap<String, String>>,
1173    out: &mut Vec<String>,
1174) {
1175    match operand {
1176        Operand::Path(path) | Operand::Size(path) => {
1177            collect_path_undefined_refs(path, attr_names, out);
1178        }
1179        Operand::ValueRef(_) => {}
1180    }
1181}
1182
1183fn collect_path_undefined_refs(
1184    path: &[PathElement],
1185    attr_names: &Option<HashMap<String, String>>,
1186    out: &mut Vec<String>,
1187) {
1188    for elem in path {
1189        if let PathElement::Attribute(name) = elem {
1190            if name.starts_with('#') {
1191                let defined = attr_names
1192                    .as_ref()
1193                    .is_some_and(|m| m.contains_key(name.as_str()));
1194                if !defined && !out.contains(name) {
1195                    out.push(name.clone());
1196                }
1197            }
1198        }
1199    }
1200}
1201
1202/// Returns true if a DynamoDB number string can be safely compared using f64.
1203/// f64 has 15-17 significant decimal digits of precision; ≤15 digit strings
1204/// are always exactly representable so no precision is lost.
1205fn can_use_f64(s: &str) -> bool {
1206    // Reject scientific notation — uncommon and complicates digit counting
1207    if s.contains('E') || s.contains('e') {
1208        return false;
1209    }
1210    // Count digit characters (skip sign and decimal point).
1211    // If total digits ≤ 15, the number fits exactly in f64.
1212    let digit_count = s.bytes().filter(|b| b.is_ascii_digit()).count();
1213    digit_count <= 15
1214}
1215
1216#[cfg(test)]
1217mod tests {
1218    use super::*;
1219    use crate::expressions::evaluate_without_tracking;
1220
1221    fn make_item(pairs: &[(&str, AttributeValue)]) -> HashMap<String, AttributeValue> {
1222        pairs
1223            .iter()
1224            .map(|(k, v)| (k.to_string(), v.clone()))
1225            .collect()
1226    }
1227
1228    fn vals(pairs: &[(&str, AttributeValue)]) -> Option<HashMap<String, AttributeValue>> {
1229        Some(make_item(pairs))
1230    }
1231
1232    fn names(pairs: &[(&str, &str)]) -> Option<HashMap<String, String>> {
1233        Some(
1234            pairs
1235                .iter()
1236                .map(|(k, v)| (k.to_string(), v.to_string()))
1237                .collect(),
1238        )
1239    }
1240
1241    #[test]
1242    fn test_simple_equality() {
1243        let expr = parse("pk = :val").unwrap();
1244        let item = make_item(&[("pk", AttributeValue::S("hello".into()))]);
1245        let av = vals(&[(":val", AttributeValue::S("hello".into()))]);
1246        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1247    }
1248
1249    #[test]
1250    fn test_inequality() {
1251        let expr = parse("pk <> :val").unwrap();
1252        let item = make_item(&[("pk", AttributeValue::S("hello".into()))]);
1253        let av = vals(&[(":val", AttributeValue::S("world".into()))]);
1254        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1255    }
1256
1257    #[test]
1258    fn test_numeric_comparison() {
1259        let expr = parse("price > :min").unwrap();
1260        let item = make_item(&[("price", AttributeValue::N("42".into()))]);
1261        let av = vals(&[(":min", AttributeValue::N("10".into()))]);
1262        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1263    }
1264
1265    #[test]
1266    fn test_between() {
1267        let expr = parse("age BETWEEN :lo AND :hi").unwrap();
1268        let item = make_item(&[("age", AttributeValue::N("25".into()))]);
1269        let av = vals(&[
1270            (":lo", AttributeValue::N("18".into())),
1271            (":hi", AttributeValue::N("65".into())),
1272        ]);
1273        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1274    }
1275
1276    #[test]
1277    fn test_in_operator() {
1278        let expr = parse("state_val IN (:s1, :s2, :s3)").unwrap();
1279        let item = make_item(&[("state_val", AttributeValue::S("active".into()))]);
1280        let av = vals(&[
1281            (":s1", AttributeValue::S("active".into())),
1282            (":s2", AttributeValue::S("pending".into())),
1283            (":s3", AttributeValue::S("closed".into())),
1284        ]);
1285        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1286    }
1287
1288    #[test]
1289    fn test_attribute_exists() {
1290        let expr = parse("attribute_exists(email)").unwrap();
1291        let item = make_item(&[("email", AttributeValue::S("a@b.com".into()))]);
1292        assert!(evaluate_without_tracking(&expr, &item, &None, &None).unwrap());
1293
1294        let empty_item: HashMap<String, AttributeValue> = HashMap::new();
1295        assert!(!evaluate_without_tracking(&expr, &empty_item, &None, &None).unwrap());
1296    }
1297
1298    #[test]
1299    fn test_attribute_not_exists() {
1300        let expr = parse("attribute_not_exists(email)").unwrap();
1301        let item: HashMap<String, AttributeValue> = HashMap::new();
1302        assert!(evaluate_without_tracking(&expr, &item, &None, &None).unwrap());
1303    }
1304
1305    #[test]
1306    fn test_begins_with() {
1307        let expr = parse("begins_with(sk, :prefix)").unwrap();
1308        let item = make_item(&[("sk", AttributeValue::S("user#123".into()))]);
1309        let av = vals(&[(":prefix", AttributeValue::S("user#".into()))]);
1310        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1311    }
1312
1313    #[test]
1314    fn test_contains_string() {
1315        let expr = parse("contains(description, :sub)").unwrap();
1316        let item = make_item(&[("description", AttributeValue::S("hello world".into()))]);
1317        let av = vals(&[(":sub", AttributeValue::S("world".into()))]);
1318        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1319    }
1320
1321    #[test]
1322    fn test_contains_string_set() {
1323        let expr = parse("contains(tags, :tag)").unwrap();
1324        let item = make_item(&[(
1325            "tags",
1326            AttributeValue::SS(vec!["rust".into(), "dynamo".into()]),
1327        )]);
1328        let av = vals(&[(":tag", AttributeValue::S("rust".into()))]);
1329        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1330    }
1331
1332    #[test]
1333    fn test_size_function() {
1334        let expr = parse("size(label) > :len").unwrap();
1335        let item = make_item(&[("label", AttributeValue::S("Alice".into()))]);
1336        let av = vals(&[(":len", AttributeValue::N("3".into()))]);
1337        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1338    }
1339
1340    #[test]
1341    fn test_and_operator() {
1342        let expr = parse("price > :min AND price < :max").unwrap();
1343        let item = make_item(&[("price", AttributeValue::N("50".into()))]);
1344        let av = vals(&[
1345            (":min", AttributeValue::N("10".into())),
1346            (":max", AttributeValue::N("100".into())),
1347        ]);
1348        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1349    }
1350
1351    #[test]
1352    fn test_or_operator() {
1353        let expr = parse("state_val = :s1 OR state_val = :s2").unwrap();
1354        let item = make_item(&[("state_val", AttributeValue::S("pending".into()))]);
1355        let av = vals(&[
1356            (":s1", AttributeValue::S("active".into())),
1357            (":s2", AttributeValue::S("pending".into())),
1358        ]);
1359        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1360    }
1361
1362    #[test]
1363    fn test_not_operator() {
1364        let expr = parse("NOT state_val = :val").unwrap();
1365        let item = make_item(&[("state_val", AttributeValue::S("active".into()))]);
1366        let av = vals(&[(":val", AttributeValue::S("closed".into()))]);
1367        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1368    }
1369
1370    #[test]
1371    fn test_expression_attribute_names() {
1372        let expr = parse("#s = :val").unwrap();
1373        let item = make_item(&[("status", AttributeValue::S("active".into()))]);
1374        let an = names(&[("#s", "status")]);
1375        let av = vals(&[(":val", AttributeValue::S("active".into()))]);
1376        assert!(evaluate_without_tracking(&expr, &item, &an, &av).unwrap());
1377    }
1378
1379    #[test]
1380    fn test_nested_path() {
1381        let expr = parse("profile.label = :val").unwrap();
1382        let mut nested = HashMap::new();
1383        nested.insert("label".to_string(), AttributeValue::S("Alice".into()));
1384        let item = make_item(&[("profile", AttributeValue::M(nested))]);
1385        let av = vals(&[(":val", AttributeValue::S("Alice".into()))]);
1386        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1387    }
1388
1389    #[test]
1390    fn test_parenthesized() {
1391        let expr = parse("(a = :x OR b = :y) AND c = :z").unwrap();
1392        let item = make_item(&[
1393            ("a", AttributeValue::S("1".into())),
1394            ("b", AttributeValue::S("2".into())),
1395            ("c", AttributeValue::S("3".into())),
1396        ]);
1397        let av = vals(&[
1398            (":x", AttributeValue::S("wrong".into())),
1399            (":y", AttributeValue::S("2".into())),
1400            (":z", AttributeValue::S("3".into())),
1401        ]);
1402        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1403    }
1404
1405    #[test]
1406    fn test_missing_attribute_is_false() {
1407        let expr = parse("nonexistent = :val").unwrap();
1408        let item: HashMap<String, AttributeValue> = HashMap::new();
1409        let av = vals(&[(":val", AttributeValue::S("x".into()))]);
1410        assert!(!evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1411    }
1412
1413    #[test]
1414    fn test_missing_attribute_ne_is_true() {
1415        let item: HashMap<String, AttributeValue> = HashMap::new();
1416        let av = vals(&[(":val", AttributeValue::S("working".into()))]);
1417        let expr = parse("nonexistent <> :val").unwrap();
1418        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1419    }
1420
1421    #[test]
1422    fn test_missing_attribute_comparisons() {
1423        let item: HashMap<String, AttributeValue> = HashMap::new();
1424        let av = vals(&[(":val", AttributeValue::S("x".into()))]);
1425        for (op, expected) in [
1426            ("=", false),
1427            ("<>", true),
1428            ("<", false),
1429            ("<=", false),
1430            (">", false),
1431            (">=", false),
1432        ] {
1433            let expr = parse(&format!("nonexistent {} :val", op)).unwrap();
1434            assert_eq!(
1435                evaluate_without_tracking(&expr, &item, &None, &av).unwrap(),
1436                expected,
1437                "operator {} on missing attribute should be {}",
1438                op,
1439                expected
1440            );
1441        }
1442    }
1443
1444    fn map_of(pairs: &[(&str, AttributeValue)]) -> AttributeValue {
1445        AttributeValue::M(
1446            pairs
1447                .iter()
1448                .map(|(k, v)| (k.to_string(), v.clone()))
1449                .collect(),
1450        )
1451    }
1452
1453    // #103: equality on Map (M) attributes always returned false because
1454    // compare_values lacked an arm for M, falling through to the catch-all.
1455    #[test]
1456    fn test_map_equality_true() {
1457        let expr = parse("#s = :p").unwrap();
1458        let item = make_item(&[(
1459            "Status",
1460            map_of(&[("Union_Case", AttributeValue::S("Passive".into()))]),
1461        )]);
1462        let an = names(&[("#s", "Status")]);
1463        let av = vals(&[(
1464            ":p",
1465            map_of(&[("Union_Case", AttributeValue::S("Passive".into()))]),
1466        )]);
1467        assert!(evaluate_without_tracking(&expr, &item, &an, &av).unwrap());
1468    }
1469
1470    #[test]
1471    fn test_map_equality_false_and_ne_true() {
1472        let item = make_item(&[(
1473            "Status",
1474            map_of(&[("Union_Case", AttributeValue::S("Passive".into()))]),
1475        )]);
1476        let an = names(&[("#s", "Status")]);
1477        let av = vals(&[(
1478            ":p",
1479            map_of(&[("Union_Case", AttributeValue::S("Active".into()))]),
1480        )]);
1481
1482        let eq = parse("#s = :p").unwrap();
1483        assert!(!evaluate_without_tracking(&eq, &item, &an, &av).unwrap());
1484
1485        let ne = parse("#s <> :p").unwrap();
1486        assert!(evaluate_without_tracking(&ne, &item, &an, &av).unwrap());
1487    }
1488
1489    #[test]
1490    fn test_map_equality_is_key_order_independent() {
1491        let item = make_item(&[(
1492            "m",
1493            map_of(&[
1494                ("a", AttributeValue::S("1".into())),
1495                ("b", AttributeValue::S("2".into())),
1496            ]),
1497        )]);
1498        // Same entries, constructed in the opposite order.
1499        let av = vals(&[(
1500            ":p",
1501            map_of(&[
1502                ("b", AttributeValue::S("2".into())),
1503                ("a", AttributeValue::S("1".into())),
1504            ]),
1505        )]);
1506        let expr = parse("m = :p").unwrap();
1507        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1508    }
1509
1510    #[test]
1511    fn test_map_equality_differing_key_sets() {
1512        // Same size, different keys: must not be equal.
1513        let item = make_item(&[("m", map_of(&[("a", AttributeValue::N("1".into()))]))]);
1514        let av = vals(&[(":p", map_of(&[("b", AttributeValue::N("1".into()))]))]);
1515        let expr = parse("m = :p").unwrap();
1516        assert!(!evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1517    }
1518
1519    #[test]
1520    fn test_nested_map_equality() {
1521        let item = make_item(&[(
1522            "m",
1523            map_of(&[("inner", map_of(&[("x", AttributeValue::S("v".into()))]))]),
1524        )]);
1525        let av = vals(&[(
1526            ":p",
1527            map_of(&[("inner", map_of(&[("x", AttributeValue::S("v".into()))]))]),
1528        )]);
1529        let expr = parse("m = :p").unwrap();
1530        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1531    }
1532
1533    #[test]
1534    fn test_map_equality_normalises_nested_numbers() {
1535        // Nested numbers compare numerically: 1 == 1.0, consistent with scalar N.
1536        let item = make_item(&[("m", map_of(&[("n", AttributeValue::N("1".into()))]))]);
1537        let av = vals(&[(":p", map_of(&[("n", AttributeValue::N("1.0".into()))]))]);
1538        let expr = parse("m = :p").unwrap();
1539        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1540    }
1541
1542    #[test]
1543    fn test_list_equality_true() {
1544        let item = make_item(&[(
1545            "l",
1546            AttributeValue::L(vec![
1547                AttributeValue::S("a".into()),
1548                AttributeValue::N("1".into()),
1549            ]),
1550        )]);
1551        let av = vals(&[(
1552            ":p",
1553            AttributeValue::L(vec![
1554                AttributeValue::S("a".into()),
1555                AttributeValue::N("1".into()),
1556            ]),
1557        )]);
1558        let expr = parse("l = :p").unwrap();
1559        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1560    }
1561
1562    #[test]
1563    fn test_list_equality_is_order_sensitive() {
1564        let item = make_item(&[(
1565            "l",
1566            AttributeValue::L(vec![
1567                AttributeValue::S("a".into()),
1568                AttributeValue::S("b".into()),
1569            ]),
1570        )]);
1571        let av = vals(&[(
1572            ":p",
1573            AttributeValue::L(vec![
1574                AttributeValue::S("b".into()),
1575                AttributeValue::S("a".into()),
1576            ]),
1577        )]);
1578        let eq = parse("l = :p").unwrap();
1579        assert!(!evaluate_without_tracking(&eq, &item, &None, &av).unwrap());
1580        let ne = parse("l <> :p").unwrap();
1581        assert!(evaluate_without_tracking(&ne, &item, &None, &av).unwrap());
1582    }
1583
1584    #[test]
1585    fn test_map_ordering_operators_are_false() {
1586        let item = make_item(&[("m", map_of(&[("a", AttributeValue::S("1".into()))]))]);
1587        let av = vals(&[(":p", map_of(&[("a", AttributeValue::S("1".into()))]))]);
1588        for op in ["<", "<=", ">", ">="] {
1589            let expr = parse(&format!("m {} :p", op)).unwrap();
1590            assert!(
1591                !evaluate_without_tracking(&expr, &item, &None, &av).unwrap(),
1592                "ordering operator {} on maps should be false",
1593                op
1594            );
1595        }
1596    }
1597
1598    #[test]
1599    fn test_list_ordering_operators_are_false() {
1600        let item = make_item(&[("l", AttributeValue::L(vec![AttributeValue::S("a".into())]))]);
1601        let av = vals(&[(":p", AttributeValue::L(vec![AttributeValue::S("a".into())]))]);
1602        for op in ["<", "<=", ">", ">="] {
1603            let expr = parse(&format!("l {} :p", op)).unwrap();
1604            assert!(
1605                !evaluate_without_tracking(&expr, &item, &None, &av).unwrap(),
1606                "ordering operator {} on lists should be false",
1607                op
1608            );
1609        }
1610    }
1611
1612    // Ne on EQUAL documents must be false. The order-sensitivity tests only cover
1613    // Ne on unequal operands, so a constant-true Ne branch would otherwise survive.
1614    #[test]
1615    fn test_ne_on_equal_map_and_list_is_false() {
1616        let map_item = make_item(&[("m", map_of(&[("a", AttributeValue::S("1".into()))]))]);
1617        let map_av = vals(&[(":p", map_of(&[("a", AttributeValue::S("1".into()))]))]);
1618        let map_ne = parse("m <> :p").unwrap();
1619        assert!(!evaluate_without_tracking(&map_ne, &map_item, &None, &map_av).unwrap());
1620
1621        let list_item = make_item(&[("l", AttributeValue::L(vec![AttributeValue::S("a".into())]))]);
1622        let list_av = vals(&[(":p", AttributeValue::L(vec![AttributeValue::S("a".into())]))]);
1623        let list_ne = parse("l <> :p").unwrap();
1624        assert!(!evaluate_without_tracking(&list_ne, &list_item, &None, &list_av).unwrap());
1625    }
1626
1627    // <> on maps with differing key sets (same length) must be true.
1628    #[test]
1629    fn test_ne_on_differing_key_sets_is_true() {
1630        let item = make_item(&[("m", map_of(&[("a", AttributeValue::N("1".into()))]))]);
1631        let av = vals(&[(":p", map_of(&[("b", AttributeValue::N("1".into()))]))]);
1632        let expr = parse("m <> :p").unwrap();
1633        assert!(evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1634    }
1635
1636    #[test]
1637    fn test_empty_map_equality() {
1638        let item = make_item(&[("m", AttributeValue::M(HashMap::new()))]);
1639        let av = vals(&[(":p", AttributeValue::M(HashMap::new()))]);
1640        let eq = parse("m = :p").unwrap();
1641        assert!(evaluate_without_tracking(&eq, &item, &None, &av).unwrap());
1642        let ne = parse("m <> :p").unwrap();
1643        assert!(!evaluate_without_tracking(&ne, &item, &None, &av).unwrap());
1644    }
1645
1646    #[test]
1647    fn test_empty_list_equality() {
1648        let item = make_item(&[("l", AttributeValue::L(vec![]))]);
1649        let av = vals(&[(":p", AttributeValue::L(vec![]))]);
1650        let eq = parse("l = :p").unwrap();
1651        assert!(evaluate_without_tracking(&eq, &item, &None, &av).unwrap());
1652        let ne = parse("l <> :p").unwrap();
1653        assert!(!evaluate_without_tracking(&ne, &item, &None, &av).unwrap());
1654    }
1655
1656    // A shared key whose values differ in type (S vs N) routes through the
1657    // cross-type catch-all on the recursive call and must compare not-equal.
1658    #[test]
1659    fn test_map_equality_type_mismatch_at_shared_key() {
1660        let item = make_item(&[("m", map_of(&[("x", AttributeValue::S("1".into()))]))]);
1661        let av = vals(&[(":p", map_of(&[("x", AttributeValue::N("1".into()))]))]);
1662        let eq = parse("m = :p").unwrap();
1663        assert!(!evaluate_without_tracking(&eq, &item, &None, &av).unwrap());
1664        let ne = parse("m <> :p").unwrap();
1665        assert!(evaluate_without_tracking(&ne, &item, &None, &av).unwrap());
1666    }
1667
1668    #[test]
1669    fn test_list_length_mismatch_is_not_equal() {
1670        let item = make_item(&[("l", AttributeValue::L(vec![AttributeValue::S("a".into())]))]);
1671        let av = vals(&[(
1672            ":p",
1673            AttributeValue::L(vec![
1674                AttributeValue::S("a".into()),
1675                AttributeValue::S("b".into()),
1676            ]),
1677        )]);
1678        let expr = parse("l = :p").unwrap();
1679        assert!(!evaluate_without_tracking(&expr, &item, &None, &av).unwrap());
1680    }
1681
1682    // IN routes through compare_values, so document operands now match correctly.
1683    #[test]
1684    fn test_in_operator_with_map_operands() {
1685        let item = make_item(&[(
1686            "m",
1687            map_of(&[("status", AttributeValue::S("active".into()))]),
1688        )]);
1689        let av = vals(&[
1690            (
1691                ":p1",
1692                map_of(&[("status", AttributeValue::S("closed".into()))]),
1693            ),
1694            (
1695                ":p2",
1696                map_of(&[("status", AttributeValue::S("active".into()))]),
1697            ),
1698        ]);
1699        let hit = parse("m IN (:p1, :p2)").unwrap();
1700        assert!(evaluate_without_tracking(&hit, &item, &None, &av).unwrap());
1701
1702        let miss_av = vals(&[
1703            (
1704                ":p1",
1705                map_of(&[("status", AttributeValue::S("closed".into()))]),
1706            ),
1707            (
1708                ":p2",
1709                map_of(&[("status", AttributeValue::S("pending".into()))]),
1710            ),
1711        ]);
1712        let miss = parse("m IN (:p1, :p2)").unwrap();
1713        assert!(!evaluate_without_tracking(&miss, &item, &None, &miss_av).unwrap());
1714    }
1715
1716    // contains(list, :v) compares each element via compare_values, so a map
1717    // element now matches deeply.
1718    #[test]
1719    fn test_contains_list_of_maps() {
1720        let item = make_item(&[(
1721            "l",
1722            AttributeValue::L(vec![
1723                map_of(&[("role", AttributeValue::S("admin".into()))]),
1724                map_of(&[("role", AttributeValue::S("viewer".into()))]),
1725            ]),
1726        )]);
1727        let hit_av = vals(&[(":p", map_of(&[("role", AttributeValue::S("admin".into()))]))]);
1728        let hit = parse("contains(l, :p)").unwrap();
1729        assert!(evaluate_without_tracking(&hit, &item, &None, &hit_av).unwrap());
1730
1731        let miss_av = vals(&[(
1732            ":p",
1733            map_of(&[("role", AttributeValue::S("unknown".into()))]),
1734        )]);
1735        let miss = parse("contains(l, :p)").unwrap();
1736        assert!(!evaluate_without_tracking(&miss, &item, &None, &miss_av).unwrap());
1737    }
1738
1739    // Number sets are compared at full precision: two 18-digit values differing only
1740    // in the last digit are distinct, where an f64 comparison would wrongly match them.
1741    #[test]
1742    fn test_number_set_equality_full_precision() {
1743        let item = make_item(&[("ns", AttributeValue::NS(vec!["100000000000000001".into()]))]);
1744
1745        let same = vals(&[(":p", AttributeValue::NS(vec!["100000000000000001".into()]))]);
1746        let eq = parse("ns = :p").unwrap();
1747        assert!(evaluate_without_tracking(&eq, &item, &None, &same).unwrap());
1748
1749        let off_by_one = vals(&[(":p", AttributeValue::NS(vec!["100000000000000002".into()]))]);
1750        let eq2 = parse("ns = :p").unwrap();
1751        assert!(!evaluate_without_tracking(&eq2, &item, &None, &off_by_one).unwrap());
1752        let ne = parse("ns <> :p").unwrap();
1753        assert!(evaluate_without_tracking(&ne, &item, &None, &off_by_one).unwrap());
1754    }
1755
1756    // Equality is numeric, not textual: 1 and 1.0 are the same set member.
1757    #[test]
1758    fn test_number_set_equality_normalises_members() {
1759        let item = make_item(&[("ns", AttributeValue::NS(vec!["1".into(), "2".into()]))]);
1760        let av = vals(&[(":p", AttributeValue::NS(vec!["1.0".into(), "2.00".into()]))]);
1761        let eq = parse("ns = :p").unwrap();
1762        assert!(evaluate_without_tracking(&eq, &item, &None, &av).unwrap());
1763    }
1764}