1use crate::actions::helpers;
2use crate::errors::{DynoxideError, Result};
3use crate::storage_backend::StorageBackend;
4use crate::types::{self, AttributeValue};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8struct UpdateWorkResult {
10 old_item: HashMap<String, AttributeValue>,
11 item: HashMap<String, AttributeValue>,
12 item_json: String,
13 size: usize,
14 old_size: Option<usize>,
18}
19
20#[derive(Debug, Default, Deserialize)]
22struct UpdateItemRequestRaw {
23 #[serde(rename = "TableName", default)]
24 table_name: Option<String>,
25 #[serde(rename = "Key", default)]
26 key: Option<HashMap<String, AttributeValue>>,
27 #[serde(rename = "UpdateExpression", default)]
28 update_expression: Option<String>,
29 #[serde(rename = "ConditionExpression", default)]
30 condition_expression: Option<String>,
31 #[serde(rename = "ExpressionAttributeNames", default)]
32 expression_attribute_names: Option<HashMap<String, String>>,
33 #[serde(
34 rename = "ExpressionAttributeValues",
35 default,
36 deserialize_with = "crate::types::deserialize_named_null_refusals"
37 )]
38 expression_attribute_values: Option<HashMap<String, AttributeValue>>,
39 #[serde(rename = "ReturnValues", default)]
40 return_values: Option<String>,
41 #[serde(rename = "ReturnConsumedCapacity", default)]
42 return_consumed_capacity: Option<String>,
43 #[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
44 return_values_on_condition_check_failure: Option<String>,
45 #[serde(rename = "ReturnItemCollectionMetrics", default)]
46 return_item_collection_metrics: Option<String>,
47 #[serde(rename = "AttributeUpdates", default)]
48 attribute_updates: Option<HashMap<String, AttributeValueUpdate>>,
49 #[serde(rename = "Expected", default)]
50 expected: Option<serde_json::Value>,
51 #[serde(rename = "ConditionalOperator", default)]
52 conditional_operator: Option<String>,
53}
54
55#[derive(Debug, Default)]
56pub struct UpdateItemRequest {
57 pub table_name: String,
58 pub key: HashMap<String, AttributeValue>,
59 pub update_expression: Option<String>,
60 pub condition_expression: Option<String>,
61 pub expression_attribute_names: Option<HashMap<String, String>>,
62 pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
63 pub return_values: Option<String>,
64 pub return_consumed_capacity: Option<String>,
65 pub return_values_on_condition_check_failure: Option<String>,
66 pub return_item_collection_metrics: Option<String>,
67 pub attribute_updates: Option<HashMap<String, AttributeValueUpdate>>,
68 pub expected: Option<serde_json::Value>,
69 pub conditional_operator: Option<String>,
70}
71
72fn first_invalid_member_enum(
80 return_values: Option<&str>,
81 return_values_on_condition_check_failure: Option<&str>,
82 return_item_collection_metrics: Option<&str>,
83 return_consumed_capacity: Option<&str>,
84) -> Option<(String, bool)> {
85 if let Some(rv) = return_values
86 && !crate::validation::RETURN_VALUES.contains(&rv)
87 {
88 return Some((crate::validation::RETURN_VALUES_REJECTION.to_string(), true));
89 }
90 if let Some(msg) = crate::validation::return_values_on_condition_check_failure_rejection(
91 return_values_on_condition_check_failure,
92 "returnValuesOnConditionCheckFailure",
93 ) {
94 return Some((msg, false));
95 }
96 if let Some(msg) =
97 crate::validation::return_item_collection_metrics_rejection(return_item_collection_metrics)
98 {
99 return Some((msg, true));
100 }
101 if let Some(msg) =
102 crate::validation::return_consumed_capacity_rejection(return_consumed_capacity)
103 {
104 return Some((msg, false));
105 }
106 None
107}
108
109impl<'de> serde::Deserialize<'de> for UpdateItemRequest {
110 fn deserialize<D: serde::Deserializer<'de>>(
111 deserializer: D,
112 ) -> std::result::Result<Self, D::Error> {
113 let raw = UpdateItemRequestRaw::deserialize(deserializer)?;
114 use crate::validation::format_validation_errors;
115
116 if let Some(message) = crate::types::malformed_condition_boolean(raw.expected.as_ref()) {
120 return Err(serde::de::Error::custom(message));
121 }
122
123 if raw.key.is_none() && raw.table_name.as_deref() != Some("") {
133 let mut errors: Vec<String> = Vec::new();
134 if let Some(msg) = crate::validation::return_consumed_capacity_rejection(
135 raw.return_consumed_capacity.as_deref(),
136 ) {
137 errors.push(msg);
138 }
139 if raw.table_name.is_none() {
140 errors.extend(crate::validation::update_item_table_name_errors(None));
141 }
142 if let Some(rv) = raw.return_values.as_deref()
143 && !crate::validation::RETURN_VALUES.contains(&rv)
144 {
145 errors.push(crate::validation::return_values_value_rejection(rv));
146 }
147 if let Some(msg) = crate::validation::return_values_on_condition_check_failure_rejection(
148 raw.return_values_on_condition_check_failure.as_deref(),
149 "returnValuesOnConditionCheckFailure",
150 ) {
151 errors.push(msg);
152 }
153 if let Some(msg) = crate::validation::return_item_collection_metrics_rejection(
154 raw.return_item_collection_metrics.as_deref(),
155 ) {
156 errors.push(msg);
157 }
158 errors.push(
159 "Value null at 'key' failed to satisfy constraint: Member must not be null"
160 .to_string(),
161 );
162 if let Some(msg) = format_validation_errors(&errors) {
163 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
164 }
165 }
166
167 if raw.table_name.is_none()
171 && let Some(rv) = raw.return_values.as_deref()
172 && !crate::validation::RETURN_VALUES.contains(&rv)
173 {
174 let mut errors = crate::validation::update_item_table_name_errors(None);
175 errors.push(crate::validation::return_values_value_rejection(rv));
176 if let Some(msg) = format_validation_errors(&errors) {
177 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
178 }
179 }
180
181 let table_name_errors =
184 crate::validation::update_item_table_name_errors(raw.table_name.as_deref());
185 if let Some(msg) = format_validation_errors(&table_name_errors) {
186 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
187 }
188 let table_name = raw.table_name.unwrap_or_default();
189
190 let mut errors = Vec::new();
191
192 if raw.key.is_none() {
194 errors.push(
195 "Value null at 'key' failed to satisfy constraint: \
196 Member must not be null"
197 .to_string(),
198 );
199 }
200
201 match first_invalid_member_enum(
206 raw.return_values.as_deref(),
207 raw.return_values_on_condition_check_failure.as_deref(),
208 raw.return_item_collection_metrics.as_deref(),
209 raw.return_consumed_capacity.as_deref(),
210 ) {
211 Some((msg, true)) => errors.push(msg),
212 Some((msg, false)) if errors.is_empty() => {
213 return Err(serde::de::Error::custom(format!("VALIDATION:{msg}")));
214 }
215 _ => {}
216 }
217
218 if let Some(msg) = format_validation_errors(&errors) {
219 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
220 }
221
222 Ok(UpdateItemRequest {
223 table_name,
224 key: raw.key.unwrap_or_default(),
225 update_expression: raw.update_expression,
226 condition_expression: raw.condition_expression,
227 expression_attribute_names: raw.expression_attribute_names,
228 expression_attribute_values: raw.expression_attribute_values,
229 return_values: raw.return_values,
230 return_consumed_capacity: raw.return_consumed_capacity,
231 return_values_on_condition_check_failure: raw.return_values_on_condition_check_failure,
232 return_item_collection_metrics: raw.return_item_collection_metrics,
233 attribute_updates: raw.attribute_updates,
234 expected: raw.expected,
235 conditional_operator: raw.conditional_operator,
236 })
237 }
238}
239
240#[derive(Debug, Clone, Default, Deserialize)]
242pub struct AttributeValueUpdate {
243 #[serde(rename = "Action", default = "default_put_action")]
244 pub action: String,
245 #[serde(rename = "Value", default)]
246 pub value: Option<AttributeValue>,
247}
248
249const ATTRIBUTE_UPDATE_ACTION_REJECTION: &str =
253 "Member must satisfy enum value set: [ADD, DELETE, PUT]' for Action";
254
255fn default_put_action() -> String {
256 "PUT".to_string()
257}
258
259#[derive(Debug, Default, Serialize)]
260pub struct UpdateItemResponse {
261 #[serde(rename = "Attributes", skip_serializing_if = "Option::is_none")]
262 pub attributes: Option<HashMap<String, AttributeValue>>,
265 #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
266 pub consumed_capacity: Option<types::ConsumedCapacity>,
267 #[serde(
268 rename = "ItemCollectionMetrics",
269 skip_serializing_if = "Option::is_none"
270 )]
271 pub item_collection_metrics: Option<crate::types::ItemCollectionMetrics>,
272}
273
274fn wrap_invalid_update_expression(err: String) -> String {
282 if err.starts_with("Invalid UpdateExpression:") {
283 err
284 } else {
285 format!("Invalid UpdateExpression: {err}")
286 }
287}
288
289pub async fn execute<S: StorageBackend>(
290 storage: &S,
291 request: UpdateItemRequest,
292) -> Result<UpdateItemResponse> {
293 let layer = crate::validation::single_write_layer(request.expected.as_ref());
297 execute_inner(storage, request, layer)
298 .await
299 .map_err(|e| crate::validation::resolve_for_layer(layer, e))
300}
301
302async fn execute_inner<S: StorageBackend>(
303 storage: &S,
304 mut request: UpdateItemRequest,
305 layer: crate::validation::ValueLayer,
306) -> Result<UpdateItemResponse> {
307 if let Some(msg) = crate::validation::format_validation_errors(
309 &crate::validation::update_item_table_name_errors(Some(&request.table_name)),
310 ) {
311 return Err(DynoxideError::ValidationException(msg));
312 }
313
314 let updates_empty = request.attribute_updates.as_ref().is_some_and(|updates| {
318 updates.iter().any(|(name, update)| {
319 name.is_empty()
320 || update
321 .value
322 .as_ref()
323 .is_some_and(crate::empty_names::value_has_empty_name)
324 })
325 });
326 crate::empty_names::check_single_item_write(
327 &request.key,
328 updates_empty,
329 request.expected.as_ref(),
330 request.expression_attribute_names.as_ref(),
331 request.expression_attribute_values.as_ref(),
332 None,
333 )?;
334
335 helpers::validate_no_null_false(&request.key)?;
338 if let Some(ref values) = request.expression_attribute_values {
339 helpers::validate_no_null_false(values)?;
340 }
341 if let Some(ref updates) = request.attribute_updates {
342 for update in updates.values() {
343 if let Some(ref value) = update.value {
344 helpers::validate_no_null_false_value(value)?;
345 }
346 }
347 }
348
349 {
351 let mut non_expr = Vec::new();
352 let mut expr_params = Vec::new();
353 if request.attribute_updates.is_some() {
354 non_expr.push("AttributeUpdates");
355 }
356 if request.expected.is_some() {
357 non_expr.push("Expected");
358 }
359 if request.conditional_operator.is_some() {
360 non_expr.push("ConditionalOperator");
361 }
362 if request.update_expression.is_some() {
363 expr_params.push("UpdateExpression");
364 }
365 if request.condition_expression.is_some() {
366 expr_params.push("ConditionExpression");
367 }
368 let no_raw_eav: Option<serde_json::Value> = None;
369 let ctx = helpers::ExpressionParamContext {
370 non_expression_params: non_expr,
371 expression_params: expr_params,
372 all_expression_param_names: vec![],
376 expression_attribute_names: &request.expression_attribute_names,
377 expression_attribute_values: &request.expression_attribute_values,
378 expression_attribute_values_raw: &no_raw_eav,
379 layer,
380 };
381 helpers::validate_expression_params(&ctx)
383 .map_err(crate::validation::ClassifiedValidationError::into_tagged)?;
384 }
385
386 if let Some(msg) = crate::validation::conditional_operator_misuse(
389 request.conditional_operator.as_deref(),
390 crate::validation::legacy_condition_count(request.expected.as_ref()),
391 ) {
392 return Err(DynoxideError::EnvelopedValidation(msg.to_string()));
393 }
394 crate::value_nesting::check_key_enveloped(&request.key)?;
398 crate::value_nesting::check_attribute_updates(
399 request
400 .attribute_updates
401 .iter()
402 .flat_map(|updates| updates.values())
403 .map(|update| update.value.as_ref()),
404 )?;
405 crate::value_nesting::check_expected(request.expected.as_ref())?;
406
407 crate::validation::validate_key_values(&request.key, layer)
411 .map_err(crate::validation::ClassifiedValidationError::into_tagged)?;
412
413 if let Some(ref updates) = request.attribute_updates {
416 let mut names: Vec<&String> = updates.keys().collect();
417 names.sort();
418 for name in names {
419 if let Some(value) = updates[name].value.as_ref()
420 && let Some(message) = crate::validation::find_duplicate_set(value, layer)
421 {
422 return Err(DynoxideError::EnvelopedValidation(message));
423 }
424 }
425 }
426
427 if request.update_expression.is_none()
429 && let Some(ref updates) = request.attribute_updates
430 {
431 if updates
437 .values()
438 .any(|update| !["ADD", "DELETE", "PUT"].contains(&update.action.as_str()))
439 {
440 return Err(DynoxideError::EnvelopedValidation(
441 ATTRIBUTE_UPDATE_ACTION_REJECTION.to_string(),
442 ));
443 }
444 for update in updates.values() {
445 let action = update.action.as_str();
446 if update.value.is_none() && action != "DELETE" {
447 return Err(DynoxideError::ValidationException(
448 "One or more parameter values were invalid: \
449 Only DELETE action is allowed when no attribute value is specified"
450 .to_string(),
451 ));
452 }
453 if action == "DELETE"
454 && let Some(ref val) = update.value
455 {
456 let type_name = match val {
457 AttributeValue::SS(_) | AttributeValue::NS(_) | AttributeValue::BS(_) => None,
458 _ => Some(val.type_name()),
459 };
460 if let Some(tn) = type_name {
461 return Err(DynoxideError::ValidationException(format!(
462 "One or more parameter values were invalid: \
463 DELETE action with value is not supported for the type {tn}"
464 )));
465 }
466 }
467 if action == "ADD"
468 && let Some(ref val) = update.value
469 {
470 let allowed = matches!(
471 val,
472 AttributeValue::N(_)
473 | AttributeValue::SS(_)
474 | AttributeValue::NS(_)
475 | AttributeValue::BS(_)
476 | AttributeValue::L(_)
477 );
478 if !allowed {
479 let tn = val.type_name();
480 return Err(DynoxideError::ValidationException(format!(
481 "One or more parameter values were invalid: \
482 ADD action is not supported for the type {tn}"
483 )));
484 }
485 }
486 }
487 }
488
489 let expected: Option<HashMap<String, helpers::ExpectedCondition>> =
497 match request.expected.as_ref() {
498 Some(v) if request.condition_expression.is_none() => helpers::read_expected(v)?,
499 _ => None,
500 };
501
502 if request.update_expression.is_none()
505 && let Some(ref expected) = expected
506 {
507 helpers::validate_expected_conditions(expected)
508 .map_err(crate::validation::tag_for_envelope)?;
509 }
510
511 if let Some(ref ce) = request.condition_expression
515 && ce.is_empty()
516 {
517 return Err(DynoxideError::EnvelopedValidation(
518 "Invalid ConditionExpression: The expression can not be empty;".to_string(),
519 ));
520 }
521
522 if let Some(ref ue) = request.update_expression
524 && ue.is_empty()
525 {
526 return Err(DynoxideError::EnvelopedValidation(
527 "Invalid UpdateExpression: The expression can not be empty;".to_string(),
528 ));
529 }
530
531 if let Some(ref ue) = request.update_expression {
538 let parsed =
539 crate::expressions::update::parse(ue).map_err(DynoxideError::EnvelopedValidation)?;
540
541 let tracker = crate::expressions::TrackedExpressionAttributes::new(
543 &request.expression_attribute_names,
544 &request.expression_attribute_values,
545 );
546 crate::expressions::update::track_references(&parsed, &tracker)
547 .map_err(|e| DynoxideError::EnvelopedValidation(wrap_invalid_update_expression(e)))?;
548
549 if let Some(ref ce) = request.condition_expression {
553 let cond_parsed = crate::expressions::condition::parse(ce).map_err(|e| {
554 DynoxideError::EnvelopedValidation(format!("Invalid ConditionExpression: {e}"))
555 })?;
556 crate::expressions::condition::track_references(&cond_parsed, &tracker).map_err(
557 |e| DynoxideError::EnvelopedValidation(format!("Invalid ConditionExpression: {e}")),
558 )?;
559 }
560
561 tracker
564 .check_unused()
565 .map_err(crate::validation::tag_for_envelope)?;
566 }
567
568 if let Some(ref ce) = request.condition_expression {
571 let parsed = crate::expressions::condition::parse(ce).map_err(|e| {
572 DynoxideError::EnvelopedValidation(format!("Invalid ConditionExpression: {e}"))
573 })?;
574 crate::expressions::condition::validate_static(
575 &parsed,
576 &request.expression_attribute_values,
577 )
578 .map_err(DynoxideError::EnvelopedValidation)?;
579 crate::expressions::condition::validate_operand_semantics(
580 &parsed,
581 &request.expression_attribute_names,
582 &request.expression_attribute_values,
583 )
584 .map_err(|e| {
585 DynoxideError::EnvelopedValidation(format!("Invalid ConditionExpression: {e}"))
586 })?;
587 }
588
589 if let Some(ref expected) = expected
591 && !expected.is_empty()
592 {
593 let converted = helpers::convert_expected_to_condition(
594 expected,
595 request.conditional_operator.as_deref(),
596 )?;
597 if !converted.expression.is_empty() {
598 request.condition_expression = Some(converted.expression);
599 let expr_values = request
600 .expression_attribute_values
601 .get_or_insert_with(HashMap::new);
602 expr_values.extend(converted.attribute_values);
603 let expr_names = request
604 .expression_attribute_names
605 .get_or_insert_with(HashMap::new);
606 expr_names.extend(converted.attribute_names);
607 }
608 }
609
610 let meta = helpers::require_table_for_item_op(storage, &request.table_name).await?;
611 let key_schema = helpers::parse_key_schema(&meta)?;
612
613 if let Some(ref rv) = request.return_values {
618 let rv_upper = rv.to_uppercase();
619 if !["NONE", "ALL_OLD", "ALL_NEW", "UPDATED_OLD", "UPDATED_NEW"]
620 .contains(&rv_upper.as_str())
621 {
622 return Err(DynoxideError::EnvelopedValidation(
623 crate::validation::RETURN_VALUES_REJECTION.to_string(),
624 ));
625 }
626 }
627
628 helpers::validate_key_only(&request.key, &key_schema)?;
630
631 let (pk, sk) = helpers::extract_key_strings(&request.key, &key_schema)?;
634
635 let legacy_attr_names: Option<Vec<String>> = request
638 .attribute_updates
639 .as_ref()
640 .map(|updates| updates.keys().cloned().collect());
641
642 let tracker = crate::expressions::TrackedExpressionAttributes::without_tracking(
646 &request.expression_attribute_names,
647 &request.expression_attribute_values,
648 );
649
650 if let Some(ref update_expr) = request.update_expression {
659 let parsed = crate::expressions::update::parse(update_expr)
660 .map_err(DynoxideError::ValidationException)?;
661 let charge = parsed
662 .request_charge(&tracker)
663 .map_err(DynoxideError::ValidationException)?;
664 if charge > types::MAX_ITEM_SIZE {
665 return Err(DynoxideError::ValidationException(
666 "Item size to update has exceeded the maximum allowed size".to_string(),
667 ));
668 }
669 }
670
671 let (
677 UpdateWorkResult {
678 old_item,
679 item,
680 item_json,
681 size,
682 old_size,
683 },
684 gsi_units,
685 lsi_units,
686 vector_bytes,
687 ) = helpers::with_write_transaction(storage, async {
688 let existing_json = storage.get_item(&request.table_name, &pk, &sk).await?;
690 let existing_item: HashMap<String, AttributeValue> = existing_json
691 .as_ref()
692 .and_then(|j| serde_json::from_str(j).ok())
693 .unwrap_or_default();
694
695 if let Some(ref cond_expr) = request.condition_expression {
699 let parsed = crate::expressions::condition::parse(cond_expr)
700 .map_err(DynoxideError::ValidationException)?;
701 let result = crate::expressions::condition::evaluate(&parsed, &existing_item, &tracker)
702 .map_err(DynoxideError::ValidationException)?;
703 if !result {
704 let return_item = if request.return_values_on_condition_check_failure.as_deref()
705 == Some("ALL_OLD")
706 && existing_json.is_some()
707 {
708 Some(existing_item.clone())
709 } else {
710 None
711 };
712 return Err(DynoxideError::ConditionalCheckFailedException(
713 "The conditional request failed".to_string(),
714 return_item,
715 ));
716 }
717 }
718
719 let mut item = existing_item;
722 if existing_json.is_none() {
723 for (k, v) in &request.key {
724 item.insert(k.clone(), v.clone());
725 }
726 }
727
728 let old_item = item.clone();
730
731 if let Some(ref update_expr) = request.update_expression {
733 let parsed = crate::expressions::update::parse(update_expr)
734 .map_err(DynoxideError::ValidationException)?;
735
736 for action in &parsed.set_actions {
739 validate_not_key_attr(
740 action.path.first(),
741 &key_schema,
742 &request.expression_attribute_names,
743 )?;
744 }
745
746 for path in &parsed.remove_actions {
748 validate_not_key_attr(
749 path.first(),
750 &key_schema,
751 &request.expression_attribute_names,
752 )?;
753 }
754
755 for action in &parsed.add_actions {
757 validate_not_key_attr(
758 action.path.first(),
759 &key_schema,
760 &request.expression_attribute_names,
761 )?;
762 }
763
764 for action in &parsed.delete_actions {
766 validate_not_key_attr(
767 action.path.first(),
768 &key_schema,
769 &request.expression_attribute_names,
770 )?;
771 }
772
773 crate::expressions::update::apply(&mut item, &parsed, &tracker)
774 .map_err(DynoxideError::ValidationException)?;
775 }
776
777 if request.update_expression.is_none()
779 && let Some(ref updates) = request.attribute_updates
780 {
781 apply_attribute_updates(&mut item, updates, &key_schema)?;
782 }
783
784 if request.update_expression.is_some() {
792 crate::validation::validate_updated_item_nesting(&item)?;
793 }
794
795 crate::validation::validate_item_attribute_values(&item)?;
801 crate::validation::normalize_item_sets(&mut item);
802
803 helpers::validate_updated_index_keys(&old_item, &item, &meta)?;
806
807 if types::item_size(&item) > types::MAX_ITEM_SIZE {
812 return Err(DynoxideError::ValidationException(
813 "Item size to update has exceeded the maximum allowed size".to_string(),
814 ));
815 }
816
817 let vector_defs = super::vector_index::parse_vector_defs(&meta)?;
821 let size = types::billed_item_size(&item, &vector_defs);
822
823 let item_json = serde_json::to_string(&item)
825 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
826 let hash_prefix = request
827 .key
828 .get(&key_schema.partition_key)
829 .map(crate::storage::compute_hash_prefix)
830 .unwrap_or_default();
831 storage
832 .put_item_with_hash(
833 &request.table_name,
834 &pk,
835 &sk,
836 &item_json,
837 size,
838 &hash_prefix,
839 )
840 .await?;
841
842 let prior_image = existing_json.is_some().then_some(&old_item);
847 let old_size = prior_image.map(|old| types::billed_item_size(old, &vector_defs));
848
849 let target = super::gsi::IndexWrite {
850 table_name: &request.table_name,
851 pk: &pk,
852 sk: &sk,
853 pk_attr: &key_schema.partition_key,
854 sk_attr: key_schema.sort_key.as_deref(),
855 old_item: prior_image,
856 capacity_mode: request.return_consumed_capacity.as_deref(),
857 };
858
859 let gsi_units =
861 super::gsi::maintain_gsis_after_write(storage, &meta, &target, &item).await?;
862
863 let lsi_units =
865 super::lsi::maintain_lsis_after_write(storage, &meta, &target, &item).await?;
866
867 let vector_bytes = super::vector_index::maintain_vector_indexes_after_write(
871 storage, &meta, &target, &item,
872 )
873 .await?;
874
875 crate::streams::record_stream_event(storage, &meta, prior_image, Some(&item)).await?;
877
878 Ok((
879 UpdateWorkResult {
880 old_item,
881 item,
882 item_json,
883 size,
884 old_size,
885 },
886 gsi_units,
887 lsi_units,
888 vector_bytes,
889 ))
890 })
891 .await?;
892
893 let return_values = request.return_values.as_deref().unwrap_or("NONE");
895 let attributes = match return_values.to_uppercase().as_str() {
896 "ALL_OLD" => Some(old_item),
897 "ALL_NEW" => Some(item),
898 "UPDATED_OLD" => {
899 if let Some(ref update_expr) = request.update_expression {
900 let parsed = crate::expressions::update::parse(update_expr)
902 .map_err(DynoxideError::ValidationException)?;
903 omit_if_empty(extract_updated_attrs(
904 &old_item,
905 &parsed,
906 &request.expression_attribute_names,
907 ))
908 } else {
909 legacy_attr_names
911 .as_ref()
912 .map(|names| extract_named_attrs(&old_item, names))
913 .and_then(omit_if_empty)
914 }
915 }
916 "UPDATED_NEW" => {
917 if let Some(ref update_expr) = request.update_expression {
918 let parsed = crate::expressions::update::parse(update_expr)
920 .map_err(DynoxideError::ValidationException)?;
921 let new_item: HashMap<String, AttributeValue> = serde_json::from_str(&item_json)
922 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
923 omit_if_empty(extract_updated_attrs(
924 &new_item,
925 &parsed,
926 &request.expression_attribute_names,
927 ))
928 } else {
929 legacy_attr_names
931 .as_ref()
932 .map(|names| {
933 let new_item: HashMap<String, AttributeValue> =
934 serde_json::from_str(&item_json).unwrap_or_default();
935 extract_named_attrs(&new_item, names)
936 })
937 .and_then(omit_if_empty)
938 }
939 }
940 _ => None, };
942
943 let pk_value = request.key.get(&key_schema.partition_key).cloned();
945 let item_collection_metrics = helpers::build_item_collection_metrics(
946 storage,
947 &meta,
948 &request.table_name,
949 &pk,
950 &key_schema.partition_key,
951 pk_value
952 .as_ref()
953 .unwrap_or(&AttributeValue::S(String::new())),
954 &request.return_item_collection_metrics,
955 )
956 .await?;
957
958 let consumed_capacity = types::consumed_capacity_with_vector_indexes(
959 &request.table_name,
960 types::table_write_capacity_units(old_size, Some(size)),
961 &gsi_units,
962 &lsi_units,
963 &vector_bytes,
964 &request.return_consumed_capacity,
965 );
966
967 Ok(UpdateItemResponse {
968 attributes,
969 consumed_capacity,
970 item_collection_metrics,
971 })
972}
973
974fn apply_attribute_updates(
981 item: &mut HashMap<String, AttributeValue>,
982 updates: &HashMap<String, AttributeValueUpdate>,
983 key_schema: &helpers::KeySchema,
984) -> Result<()> {
985 for (attr_name, update) in updates {
986 if attr_name == &key_schema.partition_key
988 || key_schema
989 .sort_key
990 .as_ref()
991 .is_some_and(|sk| sk == attr_name)
992 {
993 return Err(helpers::key_attribute_update_refusal(attr_name));
994 }
995
996 match update.action.as_str() {
997 "PUT" => {
998 if let Some(ref value) = update.value {
999 item.insert(attr_name.clone(), value.clone());
1000 }
1001 }
1002 "ADD" => {
1003 if let Some(ref add_val) = update.value {
1004 let path = vec![crate::expressions::PathElement::Attribute(
1005 attr_name.clone(),
1006 )];
1007 crate::expressions::update::apply_add_public(item, &path, add_val)
1008 .map_err(DynoxideError::ValidationException)?;
1009 }
1010 }
1011 "DELETE" => {
1012 if let Some(ref del_val) = update.value {
1013 let path = vec![crate::expressions::PathElement::Attribute(
1015 attr_name.clone(),
1016 )];
1017 crate::expressions::update::apply_delete_public(item, &path, del_val)
1018 .map_err(DynoxideError::ValidationException)?;
1019 } else {
1020 item.remove(attr_name);
1022 }
1023 }
1024 _ => {
1025 return Err(DynoxideError::EnvelopedValidation(
1026 ATTRIBUTE_UPDATE_ACTION_REJECTION.to_string(),
1027 ));
1028 }
1029 }
1030 }
1031 Ok(())
1032}
1033
1034fn extract_updated_attrs(
1043 item: &HashMap<String, AttributeValue>,
1044 expr: &crate::expressions::update::UpdateExpr,
1045 attr_names: &Option<HashMap<String, String>>,
1046) -> HashMap<String, AttributeValue> {
1047 use crate::expressions::{PathElement, resolve_path, resolve_path_elements};
1048
1049 let no_values: Option<HashMap<String, AttributeValue>> = None;
1050 let tracker =
1051 crate::expressions::TrackedExpressionAttributes::without_tracking(attr_names, &no_values);
1052
1053 let mut paths: Vec<&[PathElement]> = Vec::new();
1055 paths.extend(expr.set_actions.iter().map(|a| a.path.as_slice()));
1056 paths.extend(expr.remove_actions.iter().map(|p| p.as_slice()));
1057 paths.extend(expr.add_actions.iter().map(|a| a.path.as_slice()));
1058 paths.extend(expr.delete_actions.iter().map(|a| a.path.as_slice()));
1059
1060 let mut result = HashMap::new();
1061 for path in paths {
1062 let Ok(resolved) = resolve_path_elements(path, &tracker) else {
1063 continue;
1064 };
1065
1066 if resolved.iter().any(|e| matches!(e, PathElement::Index(_))) {
1073 if let Some(PathElement::Attribute(top)) = resolved.first()
1074 && let Some(val) = item.get(top)
1075 {
1076 result.insert(top.clone(), val.clone());
1077 }
1078 continue;
1079 }
1080
1081 if let Some(val) = resolve_path(item, &resolved) {
1082 crate::expressions::projection::insert_at_path(&mut result, &resolved, val);
1083 }
1084 }
1085
1086 result
1087}
1088
1089fn omit_if_empty(map: HashMap<String, AttributeValue>) -> Option<HashMap<String, AttributeValue>> {
1096 if map.is_empty() { None } else { Some(map) }
1097}
1098
1099fn extract_named_attrs(
1101 item: &HashMap<String, AttributeValue>,
1102 attr_names: &[String],
1103) -> HashMap<String, AttributeValue> {
1104 let mut result = HashMap::new();
1105 for name in attr_names {
1106 if let Some(val) = item.get(name) {
1107 result.insert(name.clone(), val.clone());
1108 }
1109 }
1110 result
1111}
1112
1113fn validate_not_key_attr(
1115 first_element: Option<&crate::expressions::PathElement>,
1116 key_schema: &helpers::KeySchema,
1117 expression_attribute_names: &Option<HashMap<String, String>>,
1118) -> crate::errors::Result<()> {
1119 if let Some(crate::expressions::PathElement::Attribute(name)) = first_element {
1120 let resolved_name = if name.starts_with('#') {
1121 crate::expressions::resolve_name(name, expression_attribute_names)
1122 .map_err(DynoxideError::ValidationException)?
1123 } else {
1124 name.clone()
1125 };
1126 if resolved_name == key_schema.partition_key
1127 || key_schema
1128 .sort_key
1129 .as_ref()
1130 .is_some_and(|sk| sk == &resolved_name)
1131 {
1132 return Err(helpers::key_attribute_update_refusal(&resolved_name));
1133 }
1134 }
1135 Ok(())
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use crate::actions::{create_table, put_item, update_item};
1141 use crate::storage::Storage;
1142 use crate::storage_backend::StorageBackend;
1143
1144 #[test]
1145 fn update_item_stops_at_first_invalid_enum() {
1146 let err = serde_json::from_value::<super::UpdateItemRequest>(serde_json::json!({
1149 "TableName": "_conformance_valid_table_name",
1150 "Key": {"pk": {"S": "test"}},
1151 "ReturnValues": "INVALID",
1152 "ReturnConsumedCapacity": "INVALID"
1153 }))
1154 .unwrap_err()
1155 .to_string();
1156 assert!(err.contains("1 validation error detected"), "got: {err}");
1157 assert!(err.contains("enum value set"), "got: {err}");
1158 assert!(
1161 err.contains("[ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATE_NEW, NONE]"),
1162 "got: {err}"
1163 );
1164 assert!(!err.contains("returnConsumedCapacity"), "got: {err}");
1165 }
1166
1167 #[test]
1168 fn update_item_empty_table_name_reports_only_table_name() {
1169 let err = serde_json::from_value::<super::UpdateItemRequest>(serde_json::json!({
1170 "TableName": "",
1171 "Key": {}
1172 }))
1173 .unwrap_err()
1174 .to_string();
1175 assert!(err.contains("1 validation error detected"), "got: {err}");
1176 assert!(err.to_lowercase().contains("tablename"), "got: {err}");
1177 }
1178
1179 #[test]
1182 fn update_item_rolls_back_base_write_when_gsi_fan_out_fails() {
1183 let storage = Storage::memory().unwrap();
1184
1185 let create = serde_json::from_value(serde_json::json!({
1186 "TableName": "Orders",
1187 "KeySchema": [{"AttributeName": "UserId", "KeyType": "HASH"}],
1188 "AttributeDefinitions": [
1189 {"AttributeName": "UserId", "AttributeType": "S"},
1190 {"AttributeName": "Status", "AttributeType": "S"},
1191 {"AttributeName": "Priority", "AttributeType": "S"}
1192 ],
1193 "GlobalSecondaryIndexes": [
1194 {"IndexName": "StatusIndex", "KeySchema": [{"AttributeName": "Status", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}},
1195 {"IndexName": "PriorityIndex", "KeySchema": [{"AttributeName": "Priority", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}}
1196 ]
1197 }))
1198 .unwrap();
1199 pollster::block_on(create_table::execute(&storage, create)).unwrap();
1200
1201 let put = serde_json::from_value(serde_json::json!({
1202 "TableName": "Orders",
1203 "Item": {"UserId": {"S": "u1"}, "Status": {"S": "SHIPPED"}, "Priority": {"S": "HIGH"}, "Note": {"S": "before"}}
1204 }))
1205 .unwrap();
1206 pollster::block_on(put_item::execute(&storage, put)).unwrap();
1207
1208 storage.drop_gsi_table("Orders", "PriorityIndex").unwrap();
1210
1211 let update = serde_json::from_value(serde_json::json!({
1212 "TableName": "Orders",
1213 "Key": {"UserId": {"S": "u1"}},
1214 "UpdateExpression": "SET Note = :n",
1215 "ExpressionAttributeValues": {":n": {"S": "after"}}
1216 }))
1217 .unwrap();
1218 let res = pollster::block_on(update_item::execute(&storage, update));
1219 assert!(
1220 res.is_err(),
1221 "a mid-fan-out failure must surface as an error"
1222 );
1223
1224 let rows = pollster::block_on(<Storage as StorageBackend>::scan_items(
1227 &storage,
1228 "Orders",
1229 &Default::default(),
1230 ))
1231 .unwrap();
1232 assert_eq!(
1233 rows.len(),
1234 1,
1235 "the item must still be present after rollback"
1236 );
1237 let raw = &rows[0].2;
1238 assert!(
1239 raw.contains("\"before\"") && !raw.contains("\"after\""),
1240 "update must roll back when fan-out fails, leaving the original value: {raw}"
1241 );
1242 }
1243}