Skip to main content

dynoxide/actions/
update_item.rs

1use crate::actions::helpers;
2use crate::errors::{DynoxideError, Result};
3use crate::storage::Storage;
4use crate::types::{self, AttributeValue};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Internal result from the transactional update work closure.
9struct UpdateWorkResult {
10    existing_json: Option<String>,
11    old_item: HashMap<String, AttributeValue>,
12    item: HashMap<String, AttributeValue>,
13    item_json: String,
14    size: usize,
15}
16
17/// Internal deserialization struct for detecting missing fields.
18#[derive(Debug, Default, Deserialize)]
19struct UpdateItemRequestRaw {
20    #[serde(rename = "TableName", default)]
21    table_name: Option<String>,
22    #[serde(rename = "Key", default)]
23    key: Option<HashMap<String, AttributeValue>>,
24    #[serde(rename = "UpdateExpression", default)]
25    update_expression: Option<String>,
26    #[serde(rename = "ConditionExpression", default)]
27    condition_expression: Option<String>,
28    #[serde(rename = "ExpressionAttributeNames", default)]
29    expression_attribute_names: Option<HashMap<String, String>>,
30    #[serde(rename = "ExpressionAttributeValues", default)]
31    expression_attribute_values: Option<HashMap<String, AttributeValue>>,
32    #[serde(rename = "ReturnValues", default)]
33    return_values: Option<String>,
34    #[serde(rename = "ReturnConsumedCapacity", default)]
35    return_consumed_capacity: Option<String>,
36    #[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
37    return_values_on_condition_check_failure: Option<String>,
38    #[serde(rename = "ReturnItemCollectionMetrics", default)]
39    return_item_collection_metrics: Option<String>,
40    #[serde(rename = "AttributeUpdates", default)]
41    attribute_updates: Option<HashMap<String, AttributeValueUpdate>>,
42    #[serde(rename = "Expected", default)]
43    expected: Option<serde_json::Value>,
44    #[serde(rename = "ConditionalOperator", default)]
45    conditional_operator: Option<String>,
46}
47
48#[derive(Debug, Default)]
49pub struct UpdateItemRequest {
50    pub table_name: String,
51    pub key: HashMap<String, AttributeValue>,
52    pub update_expression: Option<String>,
53    pub condition_expression: Option<String>,
54    pub expression_attribute_names: Option<HashMap<String, String>>,
55    pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
56    pub return_values: Option<String>,
57    pub return_consumed_capacity: Option<String>,
58    pub return_values_on_condition_check_failure: Option<String>,
59    pub return_item_collection_metrics: Option<String>,
60    pub attribute_updates: Option<HashMap<String, AttributeValueUpdate>>,
61    pub expected: Option<serde_json::Value>,
62    pub conditional_operator: Option<String>,
63}
64
65impl<'de> serde::Deserialize<'de> for UpdateItemRequest {
66    fn deserialize<D: serde::Deserializer<'de>>(
67        deserializer: D,
68    ) -> std::result::Result<Self, D::Error> {
69        let raw = UpdateItemRequestRaw::deserialize(deserializer)?;
70        use crate::validation::{format_validation_errors, table_name_constraint_errors};
71
72        let mut errors = Vec::new();
73
74        // Table name constraints
75        errors.extend(table_name_constraint_errors(raw.table_name.as_deref()));
76        let table_name = raw.table_name.unwrap_or_default();
77
78        // Key constraint
79        if raw.key.is_none() {
80            errors.push(
81                "Value null at 'key' failed to satisfy constraint: \
82                 Member must not be null"
83                    .to_string(),
84            );
85        }
86
87        // ReturnConsumedCapacity enum
88        if let Some(ref rcc) = raw.return_consumed_capacity {
89            if !["INDEXES", "TOTAL", "NONE"].contains(&rcc.as_str()) {
90                errors.push(format!(
91                    "Value '{}' at 'returnConsumedCapacity' failed to satisfy constraint: \
92                     Member must satisfy enum value set: [INDEXES, TOTAL, NONE]",
93                    rcc
94                ));
95            }
96        }
97
98        // ReturnValues enum
99        if let Some(ref rv) = raw.return_values {
100            if !["ALL_NEW", "UPDATED_OLD", "ALL_OLD", "NONE", "UPDATED_NEW"].contains(&rv.as_str())
101            {
102                errors.push(format!(
103                    "Value '{}' at 'returnValues' failed to satisfy constraint: \
104                     Member must satisfy enum value set: \
105                     [ALL_NEW, UPDATED_OLD, ALL_OLD, NONE, UPDATED_NEW]",
106                    rv
107                ));
108            }
109        }
110
111        // ReturnItemCollectionMetrics enum
112        if let Some(ref ricm) = raw.return_item_collection_metrics {
113            if !["SIZE", "NONE"].contains(&ricm.as_str()) {
114                errors.push(format!(
115                    "Value '{}' at 'returnItemCollectionMetrics' failed to satisfy constraint: \
116                     Member must satisfy enum value set: [SIZE, NONE]",
117                    ricm
118                ));
119            }
120        }
121
122        if let Some(msg) = format_validation_errors(&errors) {
123            return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
124        }
125
126        Ok(UpdateItemRequest {
127            table_name,
128            key: raw.key.unwrap_or_default(),
129            update_expression: raw.update_expression,
130            condition_expression: raw.condition_expression,
131            expression_attribute_names: raw.expression_attribute_names,
132            expression_attribute_values: raw.expression_attribute_values,
133            return_values: raw.return_values,
134            return_consumed_capacity: raw.return_consumed_capacity,
135            return_values_on_condition_check_failure: raw.return_values_on_condition_check_failure,
136            return_item_collection_metrics: raw.return_item_collection_metrics,
137            attribute_updates: raw.attribute_updates,
138            expected: raw.expected,
139            conditional_operator: raw.conditional_operator,
140        })
141    }
142}
143
144/// Legacy `AttributeUpdates` entry — one per attribute being modified.
145#[derive(Debug, Clone, Default, Deserialize)]
146pub struct AttributeValueUpdate {
147    #[serde(rename = "Action", default = "default_put_action")]
148    pub action: String,
149    #[serde(rename = "Value", default)]
150    pub value: Option<AttributeValue>,
151}
152
153fn default_put_action() -> String {
154    "PUT".to_string()
155}
156
157#[derive(Debug, Default, Serialize)]
158pub struct UpdateItemResponse {
159    #[serde(rename = "Attributes", skip_serializing_if = "Option::is_none")]
160    pub attributes: Option<HashMap<String, AttributeValue>>,
161    #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
162    pub consumed_capacity: Option<types::ConsumedCapacity>,
163    #[serde(
164        rename = "ItemCollectionMetrics",
165        skip_serializing_if = "Option::is_none"
166    )]
167    pub item_collection_metrics: Option<crate::types::ItemCollectionMetrics>,
168}
169
170pub fn execute(storage: &Storage, mut request: UpdateItemRequest) -> Result<UpdateItemResponse> {
171    // Validate table name format before checking existence (DynamoDB validates input first)
172    crate::validation::validate_table_name(&request.table_name)?;
173
174    // Validate expression/non-expression parameter conflicts BEFORE Expected conversion
175    {
176        let mut non_expr = Vec::new();
177        let mut expr_params = Vec::new();
178        if request.attribute_updates.is_some() {
179            non_expr.push("AttributeUpdates");
180        }
181        if request.expected.is_some() {
182            non_expr.push("Expected");
183        }
184        if request.update_expression.is_some() {
185            expr_params.push("UpdateExpression");
186        }
187        if request.condition_expression.is_some() {
188            expr_params.push("ConditionExpression");
189        }
190        let no_raw_eav: Option<serde_json::Value> = None;
191        let ctx = helpers::ExpressionParamContext {
192            non_expression_params: non_expr,
193            expression_params: expr_params,
194            all_expression_param_names: vec!["UpdateExpression", "ConditionExpression"],
195            expression_attribute_names: &request.expression_attribute_names,
196            expression_attribute_values: &request.expression_attribute_values,
197            expression_attribute_values_raw: &no_raw_eav,
198        };
199        helpers::validate_expression_params(&ctx)?;
200    }
201
202    // Validate key attribute values (unsupported datatypes, invalid numbers)
203    crate::validation::validate_key_attribute_values(&request.key)?;
204
205    // Validate legacy AttributeUpdates parameters
206    if request.update_expression.is_none() {
207        if let Some(ref updates) = request.attribute_updates {
208            for (attr_name, update) in updates {
209                let action = update.action.to_uppercase();
210                if update.value.is_none() && action != "DELETE" {
211                    return Err(DynoxideError::ValidationException(
212                        "One or more parameter values were invalid: \
213                         Only DELETE action is allowed when no attribute value is specified"
214                            .to_string(),
215                    ));
216                }
217                if action == "DELETE" {
218                    if let Some(ref val) = update.value {
219                        let type_name = match val {
220                            AttributeValue::SS(_)
221                            | AttributeValue::NS(_)
222                            | AttributeValue::BS(_) => None,
223                            _ => Some(val.type_name()),
224                        };
225                        if let Some(tn) = type_name {
226                            return Err(DynoxideError::ValidationException(format!(
227                                "One or more parameter values were invalid: \
228                                 DELETE action with value is not supported for the type {tn}"
229                            )));
230                        }
231                    }
232                }
233                if action == "ADD" {
234                    if let Some(ref val) = update.value {
235                        let allowed = matches!(
236                            val,
237                            AttributeValue::N(_)
238                                | AttributeValue::SS(_)
239                                | AttributeValue::NS(_)
240                                | AttributeValue::BS(_)
241                                | AttributeValue::L(_)
242                        );
243                        if !allowed {
244                            let tn = val.type_name();
245                            return Err(DynoxideError::ValidationException(format!(
246                                "One or more parameter values were invalid: \
247                                 ADD action is not supported for the type {tn}"
248                            )));
249                        }
250                    }
251                }
252                let _ = attr_name; // suppress unused warning
253            }
254        }
255    }
256
257    // Validate legacy Expected parameter
258    if request.condition_expression.is_none() && request.update_expression.is_none() {
259        if let Some(ref expected_val) = request.expected {
260            if let Ok(expected) = serde_json::from_value::<
261                HashMap<String, helpers::ExpectedCondition>,
262            >(expected_val.clone())
263            {
264                helpers::validate_expected_conditions(&expected)?;
265            }
266        }
267    }
268
269    // Validate empty UpdateExpression
270    if let Some(ref ue) = request.update_expression {
271        if ue.is_empty() {
272            return Err(DynoxideError::ValidationException(
273                "Invalid UpdateExpression: The expression can not be empty;".to_string(),
274            ));
275        }
276    }
277
278    // Validate empty ConditionExpression
279    if let Some(ref ce) = request.condition_expression {
280        if ce.is_empty() {
281            return Err(DynoxideError::ValidationException(
282                "Invalid ConditionExpression: The expression can not be empty;".to_string(),
283            ));
284        }
285    }
286
287    // Pre-validate UpdateExpression syntax BEFORE table lookup.
288    // DynamoDB validates expression syntax, reserved keywords, undefined attribute
289    // names/values, overlapping paths, etc. before checking table existence.
290    if let Some(ref ue) = request.update_expression {
291        let parsed =
292            crate::expressions::update::parse(ue).map_err(DynoxideError::ValidationException)?;
293
294        // Track all attribute name/value references statically (without evaluating)
295        let tracker = crate::expressions::TrackedExpressionAttributes::new(
296            &request.expression_attribute_names,
297            &request.expression_attribute_values,
298        );
299        crate::expressions::update::track_references(&parsed, &tracker)
300            .map_err(DynoxideError::ValidationException)?;
301
302        // Also walk the ConditionExpression to track its attribute usage
303        if let Some(ref ce) = request.condition_expression {
304            if let Ok(cond_parsed) = crate::expressions::condition::parse(ce) {
305                crate::expressions::condition::track_references(&cond_parsed, &tracker)
306                    .map_err(DynoxideError::ValidationException)?;
307            }
308        }
309
310        // Check for unused expression attribute names/values
311        tracker.check_unused()?;
312    }
313
314    // Statically validate ConditionExpression (syntax + BETWEEN bounds, etc.) before table lookup
315    if let Some(ref ce) = request.condition_expression {
316        let parsed = crate::expressions::condition::parse(ce).map_err(|e| {
317            DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
318        })?;
319        crate::expressions::condition::validate_static(
320            &parsed,
321            &request.expression_attribute_values,
322        )
323        .map_err(DynoxideError::ValidationException)?;
324    }
325
326    // Convert legacy Expected parameter to ConditionExpression if no expression is set
327    if request.condition_expression.is_none() {
328        if let Some(ref expected_val) = request.expected {
329            if let Ok(expected) = serde_json::from_value::<
330                HashMap<String, helpers::ExpectedCondition>,
331            >(expected_val.clone())
332            {
333                if !expected.is_empty() {
334                    let (cond_expr, values) = helpers::convert_expected_to_condition(
335                        &expected,
336                        request.conditional_operator.as_deref(),
337                    )?;
338                    if !cond_expr.is_empty() {
339                        let names = helpers::expected_attr_names(&expected);
340                        request.condition_expression = Some(cond_expr);
341                        let expr_values = request
342                            .expression_attribute_values
343                            .get_or_insert_with(HashMap::new);
344                        expr_values.extend(values);
345                        let expr_names = request
346                            .expression_attribute_names
347                            .get_or_insert_with(HashMap::new);
348                        expr_names.extend(names);
349                    }
350                }
351            }
352        }
353    }
354
355    let meta = helpers::require_table_for_item_op(storage, &request.table_name)?;
356    let key_schema = helpers::parse_key_schema(&meta)?;
357
358    // Validate ReturnValues parameter
359    if let Some(ref rv) = request.return_values {
360        let rv_upper = rv.to_uppercase();
361        if !["NONE", "ALL_OLD", "ALL_NEW", "UPDATED_OLD", "UPDATED_NEW"]
362            .contains(&rv_upper.as_str())
363        {
364            return Err(DynoxideError::ValidationException(format!(
365                "1 validation error detected: Value '{rv}' at 'returnValues' failed to satisfy constraint: \
366                 Member must satisfy enum value set: [ALL_NEW, ALL_OLD, NONE, UPDATED_NEW, UPDATED_OLD]"
367            )));
368        }
369    }
370
371    // Validate key
372    helpers::validate_key_only(&request.key, &key_schema)?;
373
374    // Extract key values
375    let (pk, sk) = helpers::extract_key_strings(&request.key, &key_schema)?;
376
377    // Collect the set of attribute names affected by the legacy AttributeUpdates
378    // parameter, used later for UPDATED_OLD / UPDATED_NEW extraction.
379    let legacy_attr_names: Option<Vec<String>> = request
380        .attribute_updates
381        .as_ref()
382        .map(|updates| updates.keys().cloned().collect());
383
384    // Wrap condition check + write in a transaction to prevent TOCTOU races
385    let has_condition = request.condition_expression.is_some();
386    if has_condition {
387        storage.begin_transaction()?;
388    }
389
390    // Execution tracker — tracking disabled because unused-reference validation was
391    // already done statically by Tracker 1 (pre-validation block above). This tracker
392    // only needs name/value resolution, not usage tracking.
393    let tracker = crate::expressions::TrackedExpressionAttributes::without_tracking(
394        &request.expression_attribute_names,
395        &request.expression_attribute_values,
396    );
397
398    // Execute the condition check + update + write atomically within a transaction.
399    // The closure captures everything from get_item through put_item.
400    let transactional_work = || -> Result<UpdateWorkResult> {
401        // Fetch existing item (or create empty one for upsert)
402        let existing_json = storage.get_item(&request.table_name, &pk, &sk)?;
403        let mut item: HashMap<String, AttributeValue> = existing_json
404            .as_ref()
405            .and_then(|j| serde_json::from_str(j).ok())
406            .unwrap_or_default();
407
408        // If item doesn't exist, populate key attributes for upsert
409        if existing_json.is_none() {
410            for (k, v) in &request.key {
411                item.insert(k.clone(), v.clone());
412            }
413        }
414
415        // Evaluate ConditionExpression against the existing item
416        if let Some(ref cond_expr) = request.condition_expression {
417            let parsed = crate::expressions::condition::parse(cond_expr)
418                .map_err(DynoxideError::ValidationException)?;
419            let result = crate::expressions::condition::evaluate(&parsed, &item, &tracker)
420                .map_err(DynoxideError::ValidationException)?;
421            if !result {
422                let return_item = if request.return_values_on_condition_check_failure.as_deref()
423                    == Some("ALL_OLD")
424                    && existing_json.is_some()
425                {
426                    Some(item.clone())
427                } else {
428                    None
429                };
430                return Err(DynoxideError::ConditionalCheckFailedException(
431                    "The conditional request failed".to_string(),
432                    return_item,
433                ));
434            }
435        }
436
437        // Save old item for ReturnValues
438        let old_item = item.clone();
439
440        // Apply UpdateExpression
441        if let Some(ref update_expr) = request.update_expression {
442            let parsed = crate::expressions::update::parse(update_expr)
443                .map_err(DynoxideError::ValidationException)?;
444
445            // Validate: cannot modify key attributes with SET
446            // (key validation uses the free function, not tracked)
447            for action in &parsed.set_actions {
448                validate_not_key_attr(
449                    action.path.first(),
450                    &key_schema,
451                    &request.expression_attribute_names,
452                )?;
453            }
454
455            // Validate: cannot REMOVE key attributes
456            for path in &parsed.remove_actions {
457                validate_not_key_attr(
458                    path.first(),
459                    &key_schema,
460                    &request.expression_attribute_names,
461                )?;
462            }
463
464            // Validate: cannot ADD to key attributes
465            for action in &parsed.add_actions {
466                validate_not_key_attr(
467                    action.path.first(),
468                    &key_schema,
469                    &request.expression_attribute_names,
470                )?;
471            }
472
473            // Validate: cannot DELETE from key attributes
474            for action in &parsed.delete_actions {
475                validate_not_key_attr(
476                    action.path.first(),
477                    &key_schema,
478                    &request.expression_attribute_names,
479                )?;
480            }
481
482            crate::expressions::update::apply(&mut item, &parsed, &tracker)
483                .map_err(DynoxideError::ValidationException)?;
484        }
485
486        // Apply legacy AttributeUpdates (if no UpdateExpression was provided)
487        if request.update_expression.is_none() {
488            if let Some(ref updates) = request.attribute_updates {
489                apply_attribute_updates(&mut item, updates, &key_schema)?;
490            }
491        }
492
493        // Note: unused expression attribute validation already done in pre-validation
494        // block (Tracker 1). Not repeated here — runtime evaluation may skip branches
495        // (e.g., if_not_exists short-circuits) which would cause false positives.
496
497        // Validate attribute values after update expression applied
498        crate::validation::validate_item_attribute_values(&item)?;
499        crate::validation::normalize_item_sets(&mut item);
500
501        // Validate updated item size
502        let size = types::item_size(&item);
503        if size > types::MAX_ITEM_SIZE {
504            return Err(DynoxideError::ValidationException(
505                "Item size to update has exceeded the maximum allowed size".to_string(),
506            ));
507        }
508
509        // Serialize and store
510        let item_json = serde_json::to_string(&item)
511            .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
512        let hash_prefix = request
513            .key
514            .get(&key_schema.partition_key)
515            .map(crate::storage::compute_hash_prefix)
516            .unwrap_or_default();
517        storage.put_item_with_hash(
518            &request.table_name,
519            &pk,
520            &sk,
521            &item_json,
522            size,
523            &hash_prefix,
524        )?;
525
526        Ok(UpdateWorkResult {
527            existing_json,
528            old_item,
529            item,
530            item_json,
531            size,
532        })
533    };
534
535    let result = transactional_work();
536
537    // Commit or rollback the condition+write transaction
538    if has_condition {
539        match result {
540            Ok(_) => storage.commit()?,
541            Err(ref _e) => {
542                let _ = storage.rollback();
543            }
544        }
545    }
546
547    let UpdateWorkResult {
548        existing_json,
549        old_item,
550        item,
551        item_json,
552        size,
553    } = result?;
554
555    // Maintain GSI tables
556    let gsi_units = super::gsi::maintain_gsis_after_write(
557        storage,
558        &request.table_name,
559        &meta,
560        &pk,
561        &sk,
562        &item,
563        &key_schema.partition_key,
564        key_schema.sort_key.as_deref(),
565    )?;
566
567    // Maintain LSI tables
568    super::lsi::maintain_lsis_after_write(
569        storage,
570        &request.table_name,
571        &meta,
572        &pk,
573        &sk,
574        &item,
575        &key_schema.partition_key,
576        key_schema.sort_key.as_deref(),
577    )?;
578
579    // Record stream event
580    let old_for_stream = if existing_json.is_some() {
581        Some(&old_item)
582    } else {
583        None
584    };
585    crate::streams::record_stream_event(storage, &meta, old_for_stream, Some(&item))?;
586
587    // Handle ReturnValues
588    let return_values = request.return_values.as_deref().unwrap_or("NONE");
589    let attributes = match return_values.to_uppercase().as_str() {
590        "ALL_OLD" => Some(old_item),
591        "ALL_NEW" => Some(item),
592        "UPDATED_OLD" => {
593            if let Some(ref update_expr) = request.update_expression {
594                // Expression-based: extract only the attributes targeted by the expression.
595                let parsed = crate::expressions::update::parse(update_expr)
596                    .map_err(DynoxideError::ValidationException)?;
597                Some(extract_updated_attrs(
598                    &old_item,
599                    &parsed,
600                    &request.expression_attribute_names,
601                ))
602            } else {
603                // Legacy AttributeUpdates: extract the named attributes from the old item.
604                legacy_attr_names
605                    .as_ref()
606                    .map(|names| extract_named_attrs(&old_item, names))
607            }
608        }
609        "UPDATED_NEW" => {
610            if let Some(ref update_expr) = request.update_expression {
611                // Expression-based: extract only the attributes targeted by the expression.
612                let parsed = crate::expressions::update::parse(update_expr)
613                    .map_err(DynoxideError::ValidationException)?;
614                let new_item: HashMap<String, AttributeValue> = serde_json::from_str(&item_json)
615                    .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
616                Some(extract_updated_attrs(
617                    &new_item,
618                    &parsed,
619                    &request.expression_attribute_names,
620                ))
621            } else {
622                // Legacy AttributeUpdates: extract the named attributes from the new item.
623                legacy_attr_names.as_ref().map(|names| {
624                    let new_item: HashMap<String, AttributeValue> =
625                        serde_json::from_str(&item_json).unwrap_or_default();
626                    extract_named_attrs(&new_item, names)
627                })
628            }
629        }
630        _ => None, // "NONE" or default
631    };
632
633    // Build item collection metrics (only for tables with LSIs)
634    let pk_value = request.key.get(&key_schema.partition_key).cloned();
635    let item_collection_metrics = helpers::build_item_collection_metrics(
636        storage,
637        &meta,
638        &request.table_name,
639        &pk,
640        &key_schema.partition_key,
641        pk_value
642            .as_ref()
643            .unwrap_or(&AttributeValue::S(String::new())),
644        &request.return_item_collection_metrics,
645    )?;
646
647    let consumed_capacity = types::consumed_capacity_with_indexes(
648        &request.table_name,
649        types::write_capacity_units(size),
650        &gsi_units,
651        &request.return_consumed_capacity,
652    );
653
654    Ok(UpdateItemResponse {
655        attributes,
656        consumed_capacity,
657        item_collection_metrics,
658    })
659}
660
661/// Apply legacy `AttributeUpdates` to the item, mutating it in place.
662///
663/// Each entry maps an attribute name to an action:
664/// - `PUT` (default): set the attribute to the given value
665/// - `ADD`: add a number or union a set
666/// - `DELETE`: remove the attribute, or remove elements from a set
667fn apply_attribute_updates(
668    item: &mut HashMap<String, AttributeValue>,
669    updates: &HashMap<String, AttributeValueUpdate>,
670    key_schema: &helpers::KeySchema,
671) -> Result<()> {
672    for (attr_name, update) in updates {
673        // Cannot modify key attributes
674        if attr_name == &key_schema.partition_key
675            || key_schema
676                .sort_key
677                .as_ref()
678                .is_some_and(|sk| sk == attr_name)
679        {
680            return Err(DynoxideError::ValidationException(format!(
681                "One or more parameter values were invalid: \
682                 Cannot update attribute {attr_name}. This attribute is part of the key"
683            )));
684        }
685
686        let action = update.action.to_uppercase();
687        match action.as_str() {
688            "PUT" => {
689                if let Some(ref value) = update.value {
690                    item.insert(attr_name.clone(), value.clone());
691                }
692            }
693            "ADD" => {
694                if let Some(ref add_val) = update.value {
695                    let path = vec![crate::expressions::PathElement::Attribute(
696                        attr_name.clone(),
697                    )];
698                    crate::expressions::update::apply_add_public(item, &path, add_val)
699                        .map_err(DynoxideError::ValidationException)?;
700                }
701            }
702            "DELETE" => {
703                if let Some(ref del_val) = update.value {
704                    // DELETE with a value: remove elements from a set
705                    let path = vec![crate::expressions::PathElement::Attribute(
706                        attr_name.clone(),
707                    )];
708                    crate::expressions::update::apply_delete_public(item, &path, del_val)
709                        .map_err(DynoxideError::ValidationException)?;
710                } else {
711                    // DELETE without a value: remove the attribute entirely
712                    item.remove(attr_name);
713                }
714            }
715            _ => {
716                return Err(DynoxideError::ValidationException(format!(
717                    "1 validation error detected: Value '{action}' at 'attributeUpdates.{attr_name}.member.action' \
718                     failed to satisfy constraint: Member must satisfy enum value set: [ADD, PUT, DELETE]"
719                )));
720            }
721        }
722    }
723    Ok(())
724}
725
726/// Extract only the attributes that were affected by the update expression.
727fn extract_updated_attrs(
728    item: &HashMap<String, AttributeValue>,
729    expr: &crate::expressions::update::UpdateExpr,
730    attr_names: &Option<HashMap<String, String>>,
731) -> HashMap<String, AttributeValue> {
732    let mut result = HashMap::new();
733
734    // SET actions
735    for action in &expr.set_actions {
736        if let Some(name) = get_top_level_name(&action.path, attr_names) {
737            if let Some(val) = item.get(&name) {
738                result.insert(name, val.clone());
739            }
740        }
741    }
742
743    // REMOVE actions
744    for path in &expr.remove_actions {
745        if let Some(name) = get_top_level_name(path, attr_names) {
746            if let Some(val) = item.get(&name) {
747                result.insert(name, val.clone());
748            }
749        }
750    }
751
752    // ADD actions
753    for action in &expr.add_actions {
754        if let Some(name) = get_top_level_name(&action.path, attr_names) {
755            if let Some(val) = item.get(&name) {
756                result.insert(name, val.clone());
757            }
758        }
759    }
760
761    // DELETE actions
762    for action in &expr.delete_actions {
763        if let Some(name) = get_top_level_name(&action.path, attr_names) {
764            if let Some(val) = item.get(&name) {
765                result.insert(name, val.clone());
766            }
767        }
768    }
769
770    result
771}
772
773/// Extract named attributes from an item (used for legacy AttributeUpdates ReturnValues).
774fn extract_named_attrs(
775    item: &HashMap<String, AttributeValue>,
776    attr_names: &[String],
777) -> HashMap<String, AttributeValue> {
778    let mut result = HashMap::new();
779    for name in attr_names {
780        if let Some(val) = item.get(name) {
781            result.insert(name.clone(), val.clone());
782        }
783    }
784    result
785}
786
787fn get_top_level_name(
788    path: &[crate::expressions::PathElement],
789    attr_names: &Option<HashMap<String, String>>,
790) -> Option<String> {
791    match path.first() {
792        Some(crate::expressions::PathElement::Attribute(name)) => {
793            if name.starts_with('#') {
794                crate::expressions::resolve_name(name, attr_names).ok()
795            } else {
796                Some(name.clone())
797            }
798        }
799        _ => None,
800    }
801}
802
803/// Validate that a path element does not target a key attribute.
804fn validate_not_key_attr(
805    first_element: Option<&crate::expressions::PathElement>,
806    key_schema: &helpers::KeySchema,
807    expression_attribute_names: &Option<HashMap<String, String>>,
808) -> crate::errors::Result<()> {
809    if let Some(crate::expressions::PathElement::Attribute(name)) = first_element {
810        let resolved_name = if name.starts_with('#') {
811            crate::expressions::resolve_name(name, expression_attribute_names)
812                .map_err(DynoxideError::ValidationException)?
813        } else {
814            name.clone()
815        };
816        if resolved_name == key_schema.partition_key
817            || key_schema
818                .sort_key
819                .as_ref()
820                .is_some_and(|sk| sk == &resolved_name)
821        {
822            return Err(DynoxideError::ValidationException(format!(
823                "One or more parameter values were invalid: Cannot update attribute {resolved_name}. This attribute is part of the key"
824            )));
825        }
826    }
827    Ok(())
828}