Skip to main content

dynoxide/expressions/
update.rs

1//! UpdateExpression parsing and evaluation.
2//!
3//! Supports SET, REMOVE, ADD, DELETE clauses.
4
5use crate::expressions::condition::parse_raw_path;
6use crate::expressions::tokenizer::{
7    Token, TokenStream, near_window_parser, near_window_tokenizer, tokenize,
8};
9use crate::expressions::{
10    PathElement, TrackedExpressionAttributes, format_path_for_error, remove_path, resolve_path,
11    resolve_path_elements, set_path,
12};
13use crate::types::AttributeValue;
14use std::collections::HashMap;
15
16/// Parsed update expression with all clause actions.
17#[derive(Debug)]
18pub struct UpdateExpr {
19    pub set_actions: Vec<SetAction>,
20    pub remove_actions: Vec<Vec<PathElement>>,
21    pub add_actions: Vec<AddAction>,
22    pub delete_actions: Vec<DeleteAction>,
23}
24
25/// A SET action: `path = value_expr`
26#[derive(Debug)]
27pub struct SetAction {
28    pub path: Vec<PathElement>,
29    pub value: SetValue,
30}
31
32/// Value expression for SET.
33#[derive(Debug)]
34pub enum SetValue {
35    /// Direct value or path reference
36    Operand(SetOperand),
37    /// `operand + operand`
38    Plus(SetOperand, SetOperand),
39    /// `operand - operand`
40    Minus(SetOperand, SetOperand),
41}
42
43/// An operand in a SET expression.
44#[derive(Debug)]
45pub enum SetOperand {
46    Path(Vec<PathElement>),
47    ValueRef(String),
48    IfNotExists(Vec<PathElement>, Box<SetOperand>),
49    ListAppend(Box<SetOperand>, Box<SetOperand>),
50    /// A parenthesised sub-expression, e.g. `(c - :v)`.
51    Group(Box<SetValue>),
52}
53
54/// An ADD action: `path :value`
55#[derive(Debug)]
56pub struct AddAction {
57    pub path: Vec<PathElement>,
58    pub value_ref: String,
59}
60
61/// A DELETE action: `path :value`
62#[derive(Debug)]
63pub struct DeleteAction {
64    pub path: Vec<PathElement>,
65    pub value_ref: String,
66}
67
68/// Parse an UpdateExpression string.
69pub fn parse(expr: &str) -> Result<UpdateExpr, String> {
70    // Prefix the size error like every other UpdateExpression error; real
71    // DynamoDB returns "Invalid UpdateExpression: Expression size has exceeded
72    // the maximum allowed size". Confirmed against real DynamoDB (eu-west-2).
73    super::check_expression_size(expr).map_err(|e| format!("Invalid UpdateExpression: {e}"))?;
74    let tokens = match tokenize(expr) {
75        Ok(t) => t,
76        Err(err) => {
77            // Tokenizer-level syntax error (e.g. stray `!` mid-expression):
78            // emit the same shape as parser-level errors, with a tokenizer-style
79            // near: window (offending byte plus at most one more non-whitespace byte).
80            let bad = &expr[err.position..err.position + err.bad_len];
81            let near = near_window_tokenizer(expr, err.position);
82            return Err(format!(
83                r#"Invalid UpdateExpression: Syntax error; token: "{bad}", near: "{near}""#
84            ));
85        }
86    };
87    let mut stream = TokenStream::new(tokens);
88
89    let mut set_actions = Vec::new();
90    let mut remove_actions = Vec::new();
91    let mut add_actions = Vec::new();
92    let mut delete_actions = Vec::new();
93
94    let mut seen_set = false;
95    let mut seen_remove = false;
96    let mut seen_add = false;
97    let mut seen_delete = false;
98
99    while !stream.at_end() {
100        match stream.peek() {
101            Some(Token::Set) => {
102                if seen_set {
103                    return Err("Invalid UpdateExpression: The \"SET\" section can only be used once in an update expression;".to_string());
104                }
105                seen_set = true;
106                stream.next();
107                parse_set_clause(&mut stream, &mut set_actions).map_err(wrap_syntax_error)?;
108            }
109            Some(Token::Remove) => {
110                if seen_remove {
111                    return Err("Invalid UpdateExpression: The \"REMOVE\" section can only be used once in an update expression;".to_string());
112                }
113                seen_remove = true;
114                stream.next();
115                parse_remove_clause(&mut stream, &mut remove_actions).map_err(wrap_syntax_error)?;
116            }
117            Some(Token::Add) => {
118                if seen_add {
119                    return Err("Invalid UpdateExpression: The \"ADD\" section can only be used once in an update expression;".to_string());
120                }
121                seen_add = true;
122                stream.next();
123                parse_add_clause(&mut stream, &mut add_actions).map_err(wrap_syntax_error)?;
124            }
125            Some(Token::Delete) => {
126                if seen_delete {
127                    return Err("Invalid UpdateExpression: The \"DELETE\" section can only be used once in an update expression;".to_string());
128                }
129                seen_delete = true;
130                stream.next();
131                parse_delete_clause(&mut stream, &mut delete_actions).map_err(wrap_syntax_error)?;
132            }
133            Some(_) => {
134                // Unexpected leading token where SET/REMOVE/ADD/DELETE was required.
135                // Build the AWS-style "token: \"X\", near: \"X Y\"" window from the
136                // offending token's span and the next token's span (if any).
137                let offending_span = stream
138                    .peek_span()
139                    .expect("peek_span must yield when peek did");
140                let bad = &expr[offending_span.start..offending_span.end()];
141                stream.next();
142                let next_span = stream.peek_span();
143                let near = near_window_parser(expr, offending_span, next_span);
144                return Err(format!(
145                    r#"Invalid UpdateExpression: Syntax error; token: "{bad}", near: "{near}""#
146                ));
147            }
148            None => break,
149        }
150    }
151
152    Ok(UpdateExpr {
153        set_actions,
154        remove_actions,
155        add_actions,
156        delete_actions,
157    })
158}
159
160/// Wrap a sub-parser error with the standard syntax error prefix,
161/// unless it already has a recognised higher-level prefix.
162fn wrap_syntax_error(err: String) -> String {
163    if err.starts_with("Invalid UpdateExpression:") {
164        err
165    } else if err.starts_with("Attribute name is a reserved keyword") {
166        format!("Invalid UpdateExpression: {err}")
167    } else {
168        format!("Invalid UpdateExpression: Syntax error; {err}")
169    }
170}
171
172/// Walk an UpdateExpr and track all attribute name and value references
173/// without actually evaluating or modifying any item. This is used for
174/// pre-validation: checking that all referenced names/values are defined,
175/// and detecting unused names/values.
176pub fn track_references(
177    expr: &UpdateExpr,
178    tracker: &TrackedExpressionAttributes,
179) -> Result<(), String> {
180    // Collect all target paths for overlap/conflict detection
181    let mut all_target_paths: Vec<Vec<PathElement>> = Vec::new();
182
183    for action in &expr.set_actions {
184        track_path_refs(&action.path, tracker)?;
185        track_set_value_refs(&action.value, tracker)?;
186        all_target_paths.push(resolve_tracked_path(&action.path, tracker));
187    }
188    for path in &expr.remove_actions {
189        track_path_refs(path, tracker)?;
190        all_target_paths.push(resolve_tracked_path(path, tracker));
191    }
192    for action in &expr.add_actions {
193        track_path_refs(&action.path, tracker)?;
194        let val = tracker.resolve_value(&action.value_ref)?;
195        // Validate ADD operand type statically
196        validate_add_type(val)?;
197        all_target_paths.push(resolve_tracked_path(&action.path, tracker));
198    }
199    for action in &expr.delete_actions {
200        track_path_refs(&action.path, tracker)?;
201        let val = tracker.resolve_value(&action.value_ref)?;
202        // Validate DELETE operand type statically
203        validate_delete_type(val)?;
204        all_target_paths.push(resolve_tracked_path(&action.path, tracker));
205    }
206
207    // Static type validation for SET value expressions
208    for action in &expr.set_actions {
209        validate_set_value_types(&action.value, tracker)?;
210    }
211
212    // Check for overlapping/conflicting paths
213    check_path_overlaps(&all_target_paths)?;
214
215    Ok(())
216}
217
218/// Validate that an ADD operand has a compatible type.
219fn validate_add_type(val: &crate::types::AttributeValue) -> Result<(), String> {
220    use crate::types::AttributeValue;
221    match val {
222        AttributeValue::N(_)
223        | AttributeValue::SS(_)
224        | AttributeValue::NS(_)
225        | AttributeValue::BS(_) => Ok(()),
226        _ => Err(format!(
227            "Invalid UpdateExpression: Incorrect operand type for operator or function; \
228             operator: ADD, operand type: {}",
229            dynamo_type_name(val)
230        )),
231    }
232}
233
234/// Validate that a DELETE operand has a compatible type.
235fn validate_delete_type(val: &crate::types::AttributeValue) -> Result<(), String> {
236    use crate::types::AttributeValue;
237    match val {
238        AttributeValue::SS(_) | AttributeValue::NS(_) | AttributeValue::BS(_) => Ok(()),
239        _ => Err(format!(
240            "Invalid UpdateExpression: Incorrect operand type for operator or function; \
241             operator: DELETE, operand type: {}",
242            dynamo_type_name(val)
243        )),
244    }
245}
246
247/// Map an AttributeValue to its DynamoDB type name for error messages.
248fn dynamo_type_name(val: &crate::types::AttributeValue) -> &'static str {
249    use crate::types::AttributeValue;
250    match val {
251        AttributeValue::S(_) => "STRING",
252        AttributeValue::N(_) => "NUMBER",
253        AttributeValue::B(_) => "BINARY",
254        AttributeValue::BOOL(_) => "BOOLEAN",
255        AttributeValue::NULL(_) => "NULL",
256        AttributeValue::SS(_) => "SS",
257        AttributeValue::NS(_) => "NS",
258        AttributeValue::BS(_) => "BS",
259        AttributeValue::L(_) => "LIST",
260        AttributeValue::M(_) => "MAP",
261    }
262}
263
264/// Validate types for SET value expressions (arithmetic, list_append).
265fn validate_set_value_types(
266    value: &SetValue,
267    tracker: &TrackedExpressionAttributes,
268) -> Result<(), String> {
269    match value {
270        SetValue::Operand(op) => validate_set_operand_types(op, tracker),
271        SetValue::Plus(left, right) => {
272            validate_arithmetic_operand(left, "+", tracker)?;
273            validate_arithmetic_operand(right, "+", tracker)
274        }
275        SetValue::Minus(left, right) => {
276            validate_arithmetic_operand(left, "-", tracker)?;
277            validate_arithmetic_operand(right, "-", tracker)
278        }
279    }
280}
281
282/// Validate that an operand used in + or - is a number (if it's a value ref).
283fn validate_arithmetic_operand(
284    operand: &SetOperand,
285    op: &str,
286    tracker: &TrackedExpressionAttributes,
287) -> Result<(), String> {
288    use crate::types::AttributeValue;
289    match operand {
290        SetOperand::ValueRef(name) => {
291            let val = tracker.resolve_value(name)?;
292            if !matches!(val, AttributeValue::N(_)) {
293                return Err(format!(
294                    "Invalid UpdateExpression: Incorrect operand type for operator or function; \
295                     operator or function: {op}, operand type: {}",
296                    dynamo_type_name(val)
297                ));
298            }
299            Ok(())
300        }
301        SetOperand::IfNotExists(_, default) => validate_set_operand_types(default, tracker),
302        SetOperand::ListAppend(a, b) => {
303            validate_list_append_operand(a, tracker)?;
304            validate_list_append_operand(b, tracker)
305        }
306        SetOperand::Path(_) => Ok(()), // Path types checked at runtime
307        // A parenthesised group resolves to a number at runtime; validate its
308        // inner expression but leave the numeric check to evaluation.
309        SetOperand::Group(inner) => validate_set_value_types(inner, tracker),
310    }
311}
312
313/// Validate types for a set operand (recursively).
314fn validate_set_operand_types(
315    operand: &SetOperand,
316    tracker: &TrackedExpressionAttributes,
317) -> Result<(), String> {
318    match operand {
319        SetOperand::ListAppend(a, b) => {
320            validate_list_append_operand(a, tracker)?;
321            validate_list_append_operand(b, tracker)
322        }
323        SetOperand::IfNotExists(_, default) => validate_set_operand_types(default, tracker),
324        SetOperand::Group(inner) => validate_set_value_types(inner, tracker),
325        _ => Ok(()),
326    }
327}
328
329/// Validate a list_append operand is a list if it's a value ref.
330fn validate_list_append_operand(
331    operand: &SetOperand,
332    tracker: &TrackedExpressionAttributes,
333) -> Result<(), String> {
334    use crate::types::AttributeValue;
335    if let SetOperand::ValueRef(name) = operand {
336        let val = tracker.resolve_value(name)?;
337        if !matches!(val, AttributeValue::L(_)) {
338            return Err(format!(
339                "Invalid UpdateExpression: Incorrect operand type for operator or function; \
340                 operator or function: list_append, operand type: {}",
341                dynamo_type_name(val)
342            ));
343        }
344    }
345    Ok(())
346}
347
348/// Resolve path elements to their final names (expanding #name refs).
349fn resolve_tracked_path(
350    path: &[PathElement],
351    tracker: &TrackedExpressionAttributes,
352) -> Vec<PathElement> {
353    path.iter()
354        .map(|elem| {
355            if let PathElement::Attribute(name) = elem {
356                if name.starts_with('#') {
357                    if let Ok(resolved) = tracker.resolve_name(name) {
358                        return PathElement::Attribute(resolved);
359                    }
360                }
361            }
362            elem.clone()
363        })
364        .collect()
365}
366
367/// Check for overlapping or conflicting document paths.
368///
369/// Two paths overlap if one is a prefix of the other (e.g., `a.b` and `a.b.c`).
370/// Two paths conflict if they share elements but diverge in type at the same
371/// position (e.g., `a[3].c` and `a.c[3]`).
372fn check_path_overlaps(paths: &[Vec<PathElement>]) -> Result<(), String> {
373    for i in 0..paths.len() {
374        for j in (i + 1)..paths.len() {
375            let a = &paths[i];
376            let b = &paths[j];
377            let min_len = a.len().min(b.len());
378
379            // Check common prefix length
380            let mut common = 0;
381            for k in 0..min_len {
382                if a[k] == b[k] {
383                    common += 1;
384                } else {
385                    break;
386                }
387            }
388
389            if common == 0 {
390                continue;
391            }
392
393            // If one path is a prefix of the other, they overlap
394            if common == a.len() || common == b.len() {
395                let (shorter, longer) = if a.len() <= b.len() { (a, b) } else { (b, a) };
396                return Err(format!(
397                    "Invalid UpdateExpression: Two document paths overlap with each other; \
398                     must remove or rewrite one of these paths; \
399                     path one: {}, path two: {}",
400                    format_path_for_error(longer),
401                    format_path_for_error(shorter)
402                ));
403            }
404
405            // If paths share a prefix but diverge, they conflict
406            if common > 0 && common < min_len && a == b {
407                return Err(format!(
408                    "Invalid UpdateExpression: Two document paths conflict with each other; \
409                     must remove or rewrite one of these paths; \
410                     path one: {}, path two: {}",
411                    format_path_for_error(a),
412                    format_path_for_error(b)
413                ));
414            }
415        }
416    }
417    Ok(())
418}
419
420fn track_path_refs(
421    path: &[PathElement],
422    tracker: &TrackedExpressionAttributes,
423) -> Result<(), String> {
424    for elem in path {
425        if let PathElement::Attribute(name) = elem {
426            if name.starts_with('#') {
427                tracker.resolve_name(name)?;
428            }
429        }
430    }
431    Ok(())
432}
433
434fn track_set_value_refs(
435    value: &SetValue,
436    tracker: &TrackedExpressionAttributes,
437) -> Result<(), String> {
438    match value {
439        SetValue::Operand(op) => track_set_operand_refs(op, tracker),
440        SetValue::Plus(left, right) | SetValue::Minus(left, right) => {
441            track_set_operand_refs(left, tracker)?;
442            track_set_operand_refs(right, tracker)
443        }
444    }
445}
446
447fn track_set_operand_refs(
448    operand: &SetOperand,
449    tracker: &TrackedExpressionAttributes,
450) -> Result<(), String> {
451    match operand {
452        SetOperand::Path(path) => track_path_refs(path, tracker),
453        SetOperand::ValueRef(name) => {
454            tracker.resolve_value(name)?;
455            Ok(())
456        }
457        SetOperand::IfNotExists(path, default) => {
458            track_path_refs(path, tracker)?;
459            track_set_operand_refs(default, tracker)
460        }
461        SetOperand::ListAppend(a, b) => {
462            track_set_operand_refs(a, tracker)?;
463            track_set_operand_refs(b, tracker)
464        }
465        SetOperand::Group(inner) => track_set_value_refs(inner, tracker),
466    }
467}
468
469/// Apply an update expression to an item (mutating it in place), tracking attribute usage.
470pub fn apply(
471    item: &mut HashMap<String, AttributeValue>,
472    expr: &UpdateExpr,
473    tracker: &TrackedExpressionAttributes,
474) -> Result<(), String> {
475    // Process SET actions.
476    //
477    // Every SET right-hand side is evaluated against the pre-update image, so
478    // that `SET a = :v, b = a` gives `b` the OLD value of `a` rather than the
479    // value assigned to `a` earlier in the same expression. DynamoDB applies
480    // the whole expression to the item as it appeared before the update, so all
481    // reads see the original snapshot. (Overlapping target paths are rejected by
482    // `check_path_overlaps`, so no SET can legitimately read another's output.)
483    let snapshot = item.clone();
484    for action in &expr.set_actions {
485        let resolved_path = resolve_path_elements(&action.path, tracker)?;
486        let value = evaluate_set_value(&action.value, &snapshot, tracker)?;
487        set_path(item, &resolved_path, value)?;
488    }
489
490    // Process REMOVE actions
491    for path in &expr.remove_actions {
492        let resolved_path = resolve_path_elements(path, tracker)?;
493        remove_path(item, &resolved_path)?;
494    }
495
496    // Process ADD actions
497    for action in &expr.add_actions {
498        let resolved_path = resolve_path_elements(&action.path, tracker)?;
499        let add_val = tracker.resolve_value(&action.value_ref)?.clone();
500        apply_add(item, &resolved_path, &add_val).map_err(|_| {
501            "An operand in the update expression has an incorrect data type".to_string()
502        })?;
503    }
504
505    // Process DELETE actions
506    for action in &expr.delete_actions {
507        let resolved_path = resolve_path_elements(&action.path, tracker)?;
508        let del_val = tracker.resolve_value(&action.value_ref)?.clone();
509        apply_delete(item, &resolved_path, &del_val).map_err(|_| {
510            "An operand in the update expression has an incorrect data type".to_string()
511        })?;
512    }
513
514    Ok(())
515}
516
517// ---------------------------------------------------------------------------
518// SET value evaluation
519// ---------------------------------------------------------------------------
520
521fn evaluate_set_value(
522    value: &SetValue,
523    item: &HashMap<String, AttributeValue>,
524    tracker: &TrackedExpressionAttributes,
525) -> Result<AttributeValue, String> {
526    match value {
527        SetValue::Operand(op) => evaluate_set_operand(op, item, tracker),
528        SetValue::Plus(left, right) => {
529            let lv = evaluate_set_operand(left, item, tracker)?;
530            let rv = evaluate_set_operand(right, item, tracker)?;
531            match (&lv, &rv) {
532                (AttributeValue::N(a), AttributeValue::N(b)) => {
533                    use bigdecimal::BigDecimal;
534                    use std::str::FromStr;
535                    let da = BigDecimal::from_str(a).map_err(|_| format!("Invalid number: {a}"))?;
536                    let db = BigDecimal::from_str(b).map_err(|_| format!("Invalid number: {b}"))?;
537                    let result = &da + &db;
538                    Ok(AttributeValue::N(format_number(&result)))
539                }
540                _ => Err("Operands for + must be numbers".to_string()),
541            }
542        }
543        SetValue::Minus(left, right) => {
544            let lv = evaluate_set_operand(left, item, tracker)?;
545            let rv = evaluate_set_operand(right, item, tracker)?;
546            match (&lv, &rv) {
547                (AttributeValue::N(a), AttributeValue::N(b)) => {
548                    use bigdecimal::BigDecimal;
549                    use std::str::FromStr;
550                    let da = BigDecimal::from_str(a).map_err(|_| format!("Invalid number: {a}"))?;
551                    let db = BigDecimal::from_str(b).map_err(|_| format!("Invalid number: {b}"))?;
552                    let result = &da - &db;
553                    Ok(AttributeValue::N(format_number(&result)))
554                }
555                _ => Err("Operands for - must be numbers".to_string()),
556            }
557        }
558    }
559}
560
561fn evaluate_set_operand(
562    operand: &SetOperand,
563    item: &HashMap<String, AttributeValue>,
564    tracker: &TrackedExpressionAttributes,
565) -> Result<AttributeValue, String> {
566    match operand {
567        SetOperand::Path(path) => {
568            let resolved = resolve_path_elements(path, tracker)?;
569            resolve_path(item, &resolved).ok_or_else(|| {
570                "The provided expression refers to an attribute that does not exist in the item"
571                    .to_string()
572            })
573        }
574        SetOperand::ValueRef(name) => Ok(tracker.resolve_value(name)?.clone()),
575        SetOperand::IfNotExists(path, default) => {
576            let resolved = resolve_path_elements(path, tracker)?;
577            match resolve_path(item, &resolved) {
578                Some(existing) => Ok(existing),
579                None => evaluate_set_operand(default, item, tracker),
580            }
581        }
582        SetOperand::ListAppend(list1, list2) => {
583            let v1 = evaluate_set_operand(list1, item, tracker)?;
584            let v2 = evaluate_set_operand(list2, item, tracker)?;
585            match (v1, v2) {
586                (AttributeValue::L(mut a), AttributeValue::L(b)) => {
587                    a.extend(b);
588                    Ok(AttributeValue::L(a))
589                }
590                _ => Err("list_append requires two list operands".to_string()),
591            }
592        }
593        SetOperand::Group(inner) => evaluate_set_value(inner, item, tracker),
594    }
595}
596
597// ---------------------------------------------------------------------------
598// ADD action
599// ---------------------------------------------------------------------------
600
601/// Public wrapper for use by legacy `AttributeUpdates` support.
602pub fn apply_add_public(
603    item: &mut HashMap<String, AttributeValue>,
604    path: &[PathElement],
605    add_val: &AttributeValue,
606) -> Result<(), String> {
607    apply_add(item, path, add_val)
608}
609
610fn apply_add(
611    item: &mut HashMap<String, AttributeValue>,
612    path: &[PathElement],
613    add_val: &AttributeValue,
614) -> Result<(), String> {
615    let existing = resolve_path(item, path);
616
617    match (existing, add_val) {
618        // Number: add to existing number or create
619        (Some(AttributeValue::N(existing_n)), AttributeValue::N(add_n)) => {
620            use bigdecimal::BigDecimal;
621            use std::str::FromStr;
622            let de = BigDecimal::from_str(&existing_n)
623                .map_err(|_| format!("Invalid number: {existing_n}"))?;
624            let da = BigDecimal::from_str(add_n).map_err(|_| format!("Invalid number: {add_n}"))?;
625            let result = &de + &da;
626            set_path(item, path, AttributeValue::N(format_number(&result)))
627        }
628        (None, AttributeValue::N(_)) => {
629            // Create with the provided value
630            set_path(item, path, add_val.clone())
631        }
632
633        // String set: union
634        (Some(AttributeValue::SS(mut existing_set)), AttributeValue::SS(add_set)) => {
635            for s in add_set {
636                if !existing_set.contains(s) {
637                    existing_set.push(s.clone());
638                }
639            }
640            set_path(item, path, AttributeValue::SS(existing_set))
641        }
642        (None, AttributeValue::SS(_)) => set_path(item, path, add_val.clone()),
643
644        // Number set: union
645        (Some(AttributeValue::NS(mut existing_set)), AttributeValue::NS(add_set)) => {
646            for n in add_set {
647                if !existing_set.contains(n) {
648                    existing_set.push(n.clone());
649                }
650            }
651            set_path(item, path, AttributeValue::NS(existing_set))
652        }
653        (None, AttributeValue::NS(_)) => set_path(item, path, add_val.clone()),
654
655        // Binary set: union
656        (Some(AttributeValue::BS(mut existing_set)), AttributeValue::BS(add_set)) => {
657            for b in add_set {
658                if !existing_set.contains(b) {
659                    existing_set.push(b.clone());
660                }
661            }
662            set_path(item, path, AttributeValue::BS(existing_set))
663        }
664        (None, AttributeValue::BS(_)) => set_path(item, path, add_val.clone()),
665
666        // List: append elements (legacy AttributeUpdates behaviour)
667        (Some(AttributeValue::L(mut existing_list)), AttributeValue::L(add_list)) => {
668            existing_list.extend(add_list.iter().cloned());
669            set_path(item, path, AttributeValue::L(existing_list))
670        }
671        (None, AttributeValue::L(_)) => set_path(item, path, add_val.clone()),
672
673        _ => Err("Type mismatch for attribute to update".to_string()),
674    }
675}
676
677// ---------------------------------------------------------------------------
678// DELETE action
679// ---------------------------------------------------------------------------
680
681/// Public wrapper for use by legacy `AttributeUpdates` support.
682pub fn apply_delete_public(
683    item: &mut HashMap<String, AttributeValue>,
684    path: &[PathElement],
685    del_val: &AttributeValue,
686) -> Result<(), String> {
687    apply_delete(item, path, del_val)
688}
689
690fn apply_delete(
691    item: &mut HashMap<String, AttributeValue>,
692    path: &[PathElement],
693    del_val: &AttributeValue,
694) -> Result<(), String> {
695    let existing = resolve_path(item, path);
696
697    match (existing, del_val) {
698        (Some(AttributeValue::SS(existing_set)), AttributeValue::SS(del_set)) => {
699            let new_set: Vec<String> = existing_set
700                .into_iter()
701                .filter(|s| !del_set.contains(s))
702                .collect();
703            if new_set.is_empty() {
704                remove_path(item, path)
705            } else {
706                set_path(item, path, AttributeValue::SS(new_set))
707            }
708        }
709        (Some(AttributeValue::NS(existing_set)), AttributeValue::NS(del_set)) => {
710            let new_set: Vec<String> = existing_set
711                .into_iter()
712                .filter(|n| !del_set.contains(n))
713                .collect();
714            if new_set.is_empty() {
715                remove_path(item, path)
716            } else {
717                set_path(item, path, AttributeValue::NS(new_set))
718            }
719        }
720        (Some(AttributeValue::BS(existing_set)), AttributeValue::BS(del_set)) => {
721            let new_set: Vec<Vec<u8>> = existing_set
722                .into_iter()
723                .filter(|b| !del_set.contains(b))
724                .collect();
725            if new_set.is_empty() {
726                remove_path(item, path)
727            } else {
728                set_path(item, path, AttributeValue::BS(new_set))
729            }
730        }
731        (None, _) => Ok(()), // Nothing to delete from
732        _ => Err("Type mismatch for attribute to update".to_string()),
733    }
734}
735
736// ---------------------------------------------------------------------------
737// Parser
738// ---------------------------------------------------------------------------
739
740fn parse_set_clause(stream: &mut TokenStream, actions: &mut Vec<SetAction>) -> Result<(), String> {
741    actions.push(parse_set_action(stream)?);
742    while matches!(stream.peek(), Some(Token::Comma)) {
743        stream.next();
744        actions.push(parse_set_action(stream)?);
745    }
746    Ok(())
747}
748
749fn parse_set_action(stream: &mut TokenStream) -> Result<SetAction, String> {
750    let path = parse_raw_path(stream)?;
751    stream.expect(&Token::Eq)?;
752    let value = parse_set_value(stream)?;
753    Ok(SetAction { path, value })
754}
755
756fn parse_set_value(stream: &mut TokenStream) -> Result<SetValue, String> {
757    let left = parse_set_operand(stream)?;
758
759    match stream.peek() {
760        Some(Token::Plus) => {
761            stream.next();
762            let right = parse_set_operand(stream)?;
763            Ok(SetValue::Plus(left, right))
764        }
765        Some(Token::Minus) => {
766            stream.next();
767            let right = parse_set_operand(stream)?;
768            Ok(SetValue::Minus(left, right))
769        }
770        _ => Ok(SetValue::Operand(left)),
771    }
772}
773
774fn parse_set_operand(stream: &mut TokenStream) -> Result<SetOperand, String> {
775    // Check for functions: if_not_exists, list_append
776    if let Some(Token::Identifier(name)) = stream.peek() {
777        let func_name = name.to_lowercase();
778        let orig_name = name.clone();
779        match func_name.as_str() {
780            "if_not_exists" => {
781                stream.next();
782                stream.expect(&Token::LParen)?;
783
784                // First argument must be a document path (not a value ref or function)
785                match stream.peek() {
786                    Some(Token::ValueRef(_)) => {
787                        return Err(
788                            "Invalid UpdateExpression: Operator or function requires a document path; \
789                             operator or function: if_not_exists".to_string()
790                        );
791                    }
792                    Some(Token::Identifier(fname))
793                        if fname.to_lowercase() == "if_not_exists"
794                            || fname.to_lowercase() == "list_append" =>
795                    {
796                        return Err(
797                            "Invalid UpdateExpression: Operator or function requires a document path; \
798                             operator or function: if_not_exists".to_string()
799                        );
800                    }
801                    _ => {}
802                }
803
804                let path = parse_raw_path(stream)?;
805
806                // Check for correct number of operands
807                if !matches!(stream.peek(), Some(Token::Comma)) {
808                    return Err(
809                        "Invalid UpdateExpression: Incorrect number of operands for operator or function; \
810                         operator or function: if_not_exists, number of operands: 1".to_string()
811                    );
812                }
813                stream.expect(&Token::Comma)?;
814                let default = parse_set_operand(stream)?;
815                stream.expect(&Token::RParen)?;
816                return Ok(SetOperand::IfNotExists(path, Box::new(default)));
817            }
818            "list_append" => {
819                stream.next();
820                stream.expect(&Token::LParen)?;
821                let list1 = parse_set_operand(stream)?;
822
823                // Check for correct number of operands
824                if !matches!(stream.peek(), Some(Token::Comma)) {
825                    return Err(
826                        "Invalid UpdateExpression: Incorrect number of operands for operator or function; \
827                         operator or function: list_append, number of operands: 1".to_string()
828                    );
829                }
830                stream.expect(&Token::Comma)?;
831                let list2 = parse_set_operand(stream)?;
832                stream.expect(&Token::RParen)?;
833                return Ok(SetOperand::ListAppend(Box::new(list1), Box::new(list2)));
834            }
835            _ => {
836                // Check if this looks like a function call (identifier followed by '(')
837                // If so, report "Invalid function name" for unknown functions.
838                let saved_pos = stream.pos();
839                stream.next();
840                if matches!(stream.peek(), Some(Token::LParen)) {
841                    return Err(format!(
842                        "Invalid UpdateExpression: Invalid function name; function: {}",
843                        orig_name
844                    ));
845                }
846                // Rewind — not a function call, treat as path
847                stream.set_pos(saved_pos);
848            }
849        }
850    }
851
852    match stream.peek() {
853        // Parenthesised sub-expression, e.g. `(c - :v)`. The contents are a full
854        // SET value (operand or arithmetic), evaluated on the same BigDecimal path.
855        Some(Token::LParen) => {
856            stream.next();
857            let inner = parse_set_value(stream)?;
858            stream.expect(&Token::RParen)?;
859            Ok(SetOperand::Group(Box::new(inner)))
860        }
861        Some(Token::ValueRef(_)) => {
862            if let Some(Token::ValueRef(name)) = stream.next().cloned() {
863                Ok(SetOperand::ValueRef(name))
864            } else {
865                unreachable!()
866            }
867        }
868        Some(Token::Identifier(_)) | Some(Token::NameRef(_)) => {
869            let path = parse_raw_path(stream)?;
870            Ok(SetOperand::Path(path))
871        }
872        Some(t) => Err(format!("Expected operand in SET, got {t}")),
873        None => Err("Expected operand in SET, got end of expression".to_string()),
874    }
875}
876
877fn parse_remove_clause(
878    stream: &mut TokenStream,
879    actions: &mut Vec<Vec<PathElement>>,
880) -> Result<(), String> {
881    actions.push(parse_raw_path(stream)?);
882    while matches!(stream.peek(), Some(Token::Comma)) {
883        stream.next();
884        actions.push(parse_raw_path(stream)?);
885    }
886    Ok(())
887}
888
889fn parse_add_clause(stream: &mut TokenStream, actions: &mut Vec<AddAction>) -> Result<(), String> {
890    actions.push(parse_add_action(stream)?);
891    while matches!(stream.peek(), Some(Token::Comma)) {
892        stream.next();
893        actions.push(parse_add_action(stream)?);
894    }
895    Ok(())
896}
897
898fn parse_add_action(stream: &mut TokenStream) -> Result<AddAction, String> {
899    let path = parse_raw_path(stream)?;
900    match stream.next() {
901        Some(Token::ValueRef(name)) => Ok(AddAction {
902            path,
903            value_ref: name.clone(),
904        }),
905        Some(t) => Err(format!("Expected value reference in ADD, got {t}")),
906        None => Err("Expected value reference in ADD, got end of expression".to_string()),
907    }
908}
909
910fn parse_delete_clause(
911    stream: &mut TokenStream,
912    actions: &mut Vec<DeleteAction>,
913) -> Result<(), String> {
914    actions.push(parse_delete_action(stream)?);
915    while matches!(stream.peek(), Some(Token::Comma)) {
916        stream.next();
917        actions.push(parse_delete_action(stream)?);
918    }
919    Ok(())
920}
921
922fn parse_delete_action(stream: &mut TokenStream) -> Result<DeleteAction, String> {
923    let path = parse_raw_path(stream)?;
924    match stream.next() {
925        Some(Token::ValueRef(name)) => Ok(DeleteAction {
926            path,
927            value_ref: name.clone(),
928        }),
929        Some(t) => Err(format!("Expected value reference in DELETE, got {t}")),
930        None => Err("Expected value reference in DELETE, got end of expression".to_string()),
931    }
932}
933
934/// Format a BigDecimal number, stripping unnecessary trailing zeros.
935/// DynamoDB returns numbers without scientific notation.
936fn format_number(n: &bigdecimal::BigDecimal) -> String {
937    let normalized = n.normalized();
938    // Force scale >= 0 so BigDecimal renders without scientific notation.
939    // When the exponent is negative (large integer like 1e38), with_scale(0)
940    // expands to full decimal digits.
941    if normalized.as_bigint_and_exponent().1 < 0 {
942        normalized.with_scale(0).to_string()
943    } else {
944        normalized.to_string()
945    }
946}
947
948#[cfg(test)]
949mod tests {
950    use super::*;
951
952    fn make_item(pairs: &[(&str, AttributeValue)]) -> HashMap<String, AttributeValue> {
953        pairs
954            .iter()
955            .map(|(k, v)| (k.to_string(), v.clone()))
956            .collect()
957    }
958
959    fn vals(pairs: &[(&str, AttributeValue)]) -> Option<HashMap<String, AttributeValue>> {
960        Some(make_item(pairs))
961    }
962
963    fn make_tracker<'a>(
964        names: &'a Option<HashMap<String, String>>,
965        values: &'a Option<HashMap<String, AttributeValue>>,
966    ) -> TrackedExpressionAttributes<'a> {
967        TrackedExpressionAttributes::new(names, values)
968    }
969
970    #[test]
971    fn test_set_simple() {
972        let expr = parse("SET label = :val").unwrap();
973        assert_eq!(expr.set_actions.len(), 1);
974        assert!(expr.remove_actions.is_empty());
975    }
976
977    #[test]
978    fn test_set_multiple() {
979        let expr = parse("SET a = :v1, b = :v2").unwrap();
980        assert_eq!(expr.set_actions.len(), 2);
981    }
982
983    #[test]
984    fn test_set_arithmetic_plus() {
985        let expr = parse("SET tally = tally + :inc").unwrap();
986        let mut item = make_item(&[
987            ("pk", AttributeValue::S("k".into())),
988            ("tally", AttributeValue::N("10".into())),
989        ]);
990        let av = vals(&[(":inc", AttributeValue::N("5".into()))]);
991        let no_names = None;
992        let tracker = make_tracker(&no_names, &av);
993        apply(&mut item, &expr, &tracker).unwrap();
994        assert_eq!(item["tally"], AttributeValue::N("15".into()));
995    }
996
997    #[test]
998    fn test_set_arithmetic_minus() {
999        let expr = parse("SET price = price - :discount").unwrap();
1000        let mut item = make_item(&[
1001            ("pk", AttributeValue::S("k".into())),
1002            ("price", AttributeValue::N("100".into())),
1003        ]);
1004        let av = vals(&[(":discount", AttributeValue::N("25".into()))]);
1005        let no_names = None;
1006        let tracker = make_tracker(&no_names, &av);
1007        apply(&mut item, &expr, &tracker).unwrap();
1008        assert_eq!(item["price"], AttributeValue::N("75".into()));
1009    }
1010
1011    #[test]
1012    fn test_set_if_not_exists() {
1013        let expr = parse("SET hits = if_not_exists(hits, :zero)").unwrap();
1014        let mut item = make_item(&[("pk", AttributeValue::S("k".into()))]);
1015        let av = vals(&[(":zero", AttributeValue::N("0".into()))]);
1016        let no_names = None;
1017        let tracker = make_tracker(&no_names, &av);
1018        apply(&mut item, &expr, &tracker).unwrap();
1019        assert_eq!(item["hits"], AttributeValue::N("0".into()));
1020
1021        // Apply again — existing value should be preserved
1022        let tracker2 = make_tracker(&no_names, &av);
1023        apply(&mut item, &expr, &tracker2).unwrap();
1024        assert_eq!(item["hits"], AttributeValue::N("0".into()));
1025    }
1026
1027    #[test]
1028    fn test_set_list_append() {
1029        let expr = parse("SET entries = list_append(entries, :new)").unwrap();
1030        let mut item = make_item(&[
1031            ("pk", AttributeValue::S("k".into())),
1032            (
1033                "entries",
1034                AttributeValue::L(vec![AttributeValue::S("a".into())]),
1035            ),
1036        ]);
1037        let av = vals(&[(
1038            ":new",
1039            AttributeValue::L(vec![AttributeValue::S("b".into())]),
1040        )]);
1041        let no_names = None;
1042        let tracker = make_tracker(&no_names, &av);
1043        apply(&mut item, &expr, &tracker).unwrap();
1044        if let AttributeValue::L(list) = &item["entries"] {
1045            assert_eq!(list.len(), 2);
1046        } else {
1047            panic!("Expected list");
1048        }
1049    }
1050
1051    #[test]
1052    fn test_remove() {
1053        let expr = parse("REMOVE attr1, attr2").unwrap();
1054        let mut item = make_item(&[
1055            ("pk", AttributeValue::S("k".into())),
1056            ("attr1", AttributeValue::S("a".into())),
1057            ("attr2", AttributeValue::S("b".into())),
1058            ("attr3", AttributeValue::S("c".into())),
1059        ]);
1060        let no_names = None;
1061        let no_values = None;
1062        let tracker = make_tracker(&no_names, &no_values);
1063        apply(&mut item, &expr, &tracker).unwrap();
1064        assert!(!item.contains_key("attr1"));
1065        assert!(!item.contains_key("attr2"));
1066        assert!(item.contains_key("attr3"));
1067    }
1068
1069    #[test]
1070    fn test_add_number() {
1071        let expr = parse("ADD tally :inc").unwrap();
1072        let mut item = make_item(&[
1073            ("pk", AttributeValue::S("k".into())),
1074            ("tally", AttributeValue::N("10".into())),
1075        ]);
1076        let av = vals(&[(":inc", AttributeValue::N("5".into()))]);
1077        let no_names = None;
1078        let tracker = make_tracker(&no_names, &av);
1079        apply(&mut item, &expr, &tracker).unwrap();
1080        assert_eq!(item["tally"], AttributeValue::N("15".into()));
1081    }
1082
1083    #[test]
1084    fn test_add_number_create() {
1085        let expr = parse("ADD tally :val").unwrap();
1086        let mut item = make_item(&[("pk", AttributeValue::S("k".into()))]);
1087        let av = vals(&[(":val", AttributeValue::N("1".into()))]);
1088        let no_names = None;
1089        let tracker = make_tracker(&no_names, &av);
1090        apply(&mut item, &expr, &tracker).unwrap();
1091        assert_eq!(item["tally"], AttributeValue::N("1".into()));
1092    }
1093
1094    #[test]
1095    fn test_add_string_set() {
1096        let expr = parse("ADD colors :new_colors").unwrap();
1097        let mut item = make_item(&[
1098            ("pk", AttributeValue::S("k".into())),
1099            (
1100                "colors",
1101                AttributeValue::SS(vec!["red".into(), "blue".into()]),
1102            ),
1103        ]);
1104        let av = vals(&[(
1105            ":new_colors",
1106            AttributeValue::SS(vec!["blue".into(), "green".into()]),
1107        )]);
1108        let no_names = None;
1109        let tracker = make_tracker(&no_names, &av);
1110        apply(&mut item, &expr, &tracker).unwrap();
1111        if let AttributeValue::SS(set) = &item["colors"] {
1112            assert_eq!(set.len(), 3); // red, blue, green (blue deduplicated)
1113            assert!(set.contains(&"green".to_string()));
1114        } else {
1115            panic!("Expected SS");
1116        }
1117    }
1118
1119    #[test]
1120    fn test_delete_string_set() {
1121        let expr = parse("DELETE colors :remove").unwrap();
1122        let mut item = make_item(&[
1123            ("pk", AttributeValue::S("k".into())),
1124            (
1125                "colors",
1126                AttributeValue::SS(vec!["red".into(), "blue".into(), "green".into()]),
1127            ),
1128        ]);
1129        let av = vals(&[(
1130            ":remove",
1131            AttributeValue::SS(vec!["blue".into(), "green".into()]),
1132        )]);
1133        let no_names = None;
1134        let tracker = make_tracker(&no_names, &av);
1135        apply(&mut item, &expr, &tracker).unwrap();
1136        if let AttributeValue::SS(set) = &item["colors"] {
1137            assert_eq!(set, &vec!["red".to_string()]);
1138        } else {
1139            panic!("Expected SS");
1140        }
1141    }
1142
1143    #[test]
1144    fn test_combined_set_remove() {
1145        let expr = parse("SET label = :name REMOVE old_attr").unwrap();
1146        assert_eq!(expr.set_actions.len(), 1);
1147        assert_eq!(expr.remove_actions.len(), 1);
1148    }
1149
1150    #[test]
1151    fn test_duplicate_clause_error() {
1152        let result = parse("SET a = :v SET b = :w");
1153        assert!(result.is_err());
1154        assert!(result.unwrap_err().contains("only be used once"));
1155    }
1156
1157    /// #35(a): a later SET reads the pre-update value of an earlier target.
1158    #[test]
1159    fn test_set_reads_pre_update_snapshot() {
1160        let expr = parse("SET a = :v, b = a").unwrap();
1161        let mut item = make_item(&[
1162            ("pk", AttributeValue::S("k".into())),
1163            ("a", AttributeValue::S("OLD".into())),
1164        ]);
1165        let av = vals(&[(":v", AttributeValue::S("NEW".into()))]);
1166        let no_names = None;
1167        let tracker = make_tracker(&no_names, &av);
1168        apply(&mut item, &expr, &tracker).unwrap();
1169        assert_eq!(item["a"], AttributeValue::S("NEW".into()));
1170        assert_eq!(item["b"], AttributeValue::S("OLD".into()));
1171    }
1172
1173    /// #35(b): a parenthesised arithmetic group parses and evaluates.
1174    #[test]
1175    fn test_set_parenthesised_arithmetic() {
1176        let expr = parse("SET c = (c - :v)").unwrap();
1177        let mut item = make_item(&[
1178            ("pk", AttributeValue::S("k".into())),
1179            ("c", AttributeValue::N("10".into())),
1180        ]);
1181        let av = vals(&[(":v", AttributeValue::N("3".into()))]);
1182        let no_names = None;
1183        let tracker = make_tracker(&no_names, &av);
1184        apply(&mut item, &expr, &tracker).unwrap();
1185        assert_eq!(item["c"], AttributeValue::N("7".into()));
1186    }
1187
1188    /// #35(b): high-precision arithmetic inside a group stays exact (BigDecimal path).
1189    #[test]
1190    fn test_set_parenthesised_arithmetic_bigdecimal() {
1191        let expr = parse("SET c = (c + :v)").unwrap();
1192        let mut item = make_item(&[
1193            ("pk", AttributeValue::S("k".into())),
1194            ("c", AttributeValue::N("100000000000000000000".into())),
1195        ]);
1196        let av = vals(&[(":v", AttributeValue::N("1".into()))]);
1197        let no_names = None;
1198        let tracker = make_tracker(&no_names, &av);
1199        apply(&mut item, &expr, &tracker).unwrap();
1200        assert_eq!(item["c"], AttributeValue::N("100000000000000000001".into()));
1201    }
1202}