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