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