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}
15
16#[derive(Debug, Default, Deserialize)]
18struct UpdateItemRequestRaw {
19 #[serde(rename = "TableName", default)]
20 table_name: Option<String>,
21 #[serde(rename = "Key", default)]
22 key: Option<HashMap<String, AttributeValue>>,
23 #[serde(rename = "UpdateExpression", default)]
24 update_expression: Option<String>,
25 #[serde(rename = "ConditionExpression", default)]
26 condition_expression: Option<String>,
27 #[serde(rename = "ExpressionAttributeNames", default)]
28 expression_attribute_names: Option<HashMap<String, String>>,
29 #[serde(rename = "ExpressionAttributeValues", default)]
30 expression_attribute_values: Option<HashMap<String, AttributeValue>>,
31 #[serde(rename = "ReturnValues", default)]
32 return_values: Option<String>,
33 #[serde(rename = "ReturnConsumedCapacity", default)]
34 return_consumed_capacity: Option<String>,
35 #[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
36 return_values_on_condition_check_failure: Option<String>,
37 #[serde(rename = "ReturnItemCollectionMetrics", default)]
38 return_item_collection_metrics: Option<String>,
39 #[serde(rename = "AttributeUpdates", default)]
40 attribute_updates: Option<HashMap<String, AttributeValueUpdate>>,
41 #[serde(rename = "Expected", default)]
42 expected: Option<serde_json::Value>,
43 #[serde(rename = "ConditionalOperator", default)]
44 conditional_operator: Option<String>,
45}
46
47#[derive(Debug, Default)]
48pub struct UpdateItemRequest {
49 pub table_name: String,
50 pub key: HashMap<String, AttributeValue>,
51 pub update_expression: Option<String>,
52 pub condition_expression: Option<String>,
53 pub expression_attribute_names: Option<HashMap<String, String>>,
54 pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
55 pub return_values: Option<String>,
56 pub return_consumed_capacity: Option<String>,
57 pub return_values_on_condition_check_failure: Option<String>,
58 pub return_item_collection_metrics: Option<String>,
59 pub attribute_updates: Option<HashMap<String, AttributeValueUpdate>>,
60 pub expected: Option<serde_json::Value>,
61 pub conditional_operator: Option<String>,
62}
63
64fn first_invalid_return_enum(
67 return_values: Option<&str>,
68 return_consumed_capacity: Option<&str>,
69 return_item_collection_metrics: Option<&str>,
70) -> Option<String> {
71 if let Some(rv) = return_values {
72 if !["ALL_NEW", "UPDATED_OLD", "ALL_OLD", "NONE", "UPDATED_NEW"].contains(&rv) {
73 return Some(format!(
74 "Value '{}' at 'returnValues' failed to satisfy constraint: \
75 Member must satisfy enum value set: \
76 [ALL_NEW, UPDATED_OLD, ALL_OLD, NONE, UPDATED_NEW]",
77 rv
78 ));
79 }
80 }
81 if let Some(rcc) = return_consumed_capacity {
82 if !["INDEXES", "TOTAL", "NONE"].contains(&rcc) {
83 return Some(format!(
84 "Value '{}' at 'returnConsumedCapacity' failed to satisfy constraint: \
85 Member must satisfy enum value set: [INDEXES, TOTAL, NONE]",
86 rcc
87 ));
88 }
89 }
90 if let Some(ricm) = return_item_collection_metrics {
91 if !["SIZE", "NONE"].contains(&ricm) {
92 return Some(format!(
93 "Value '{}' at 'returnItemCollectionMetrics' failed to satisfy constraint: \
94 Member must satisfy enum value set: [SIZE, NONE]",
95 ricm
96 ));
97 }
98 }
99 None
100}
101
102impl<'de> serde::Deserialize<'de> for UpdateItemRequest {
103 fn deserialize<D: serde::Deserializer<'de>>(
104 deserializer: D,
105 ) -> std::result::Result<Self, D::Error> {
106 let raw = UpdateItemRequestRaw::deserialize(deserializer)?;
107 use crate::validation::{
108 TableNameContext, format_validation_errors, table_name_constraint_errors,
109 };
110
111 let table_name_errors =
114 table_name_constraint_errors(raw.table_name.as_deref(), TableNameContext::ReadWrite);
115 if let Some(msg) = format_validation_errors(&table_name_errors) {
116 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
117 }
118 let table_name = raw.table_name.unwrap_or_default();
119
120 let mut errors = Vec::new();
121
122 if raw.key.is_none() {
124 errors.push(
125 "Value null at 'key' failed to satisfy constraint: \
126 Member must not be null"
127 .to_string(),
128 );
129 }
130
131 if let Some(enum_err) = first_invalid_return_enum(
134 raw.return_values.as_deref(),
135 raw.return_consumed_capacity.as_deref(),
136 raw.return_item_collection_metrics.as_deref(),
137 ) {
138 errors.push(enum_err);
139 }
140
141 if let Some(msg) = format_validation_errors(&errors) {
142 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
143 }
144
145 Ok(UpdateItemRequest {
146 table_name,
147 key: raw.key.unwrap_or_default(),
148 update_expression: raw.update_expression,
149 condition_expression: raw.condition_expression,
150 expression_attribute_names: raw.expression_attribute_names,
151 expression_attribute_values: raw.expression_attribute_values,
152 return_values: raw.return_values,
153 return_consumed_capacity: raw.return_consumed_capacity,
154 return_values_on_condition_check_failure: raw.return_values_on_condition_check_failure,
155 return_item_collection_metrics: raw.return_item_collection_metrics,
156 attribute_updates: raw.attribute_updates,
157 expected: raw.expected,
158 conditional_operator: raw.conditional_operator,
159 })
160 }
161}
162
163#[derive(Debug, Clone, Default, Deserialize)]
165pub struct AttributeValueUpdate {
166 #[serde(rename = "Action", default = "default_put_action")]
167 pub action: String,
168 #[serde(rename = "Value", default)]
169 pub value: Option<AttributeValue>,
170}
171
172fn default_put_action() -> String {
173 "PUT".to_string()
174}
175
176#[derive(Debug, Default, Serialize)]
177pub struct UpdateItemResponse {
178 #[serde(rename = "Attributes", skip_serializing_if = "Option::is_none")]
179 pub attributes: Option<HashMap<String, AttributeValue>>,
180 #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
181 pub consumed_capacity: Option<types::ConsumedCapacity>,
182 #[serde(
183 rename = "ItemCollectionMetrics",
184 skip_serializing_if = "Option::is_none"
185 )]
186 pub item_collection_metrics: Option<crate::types::ItemCollectionMetrics>,
187}
188
189fn wrap_invalid_update_expression(err: String) -> String {
197 if err.starts_with("Invalid UpdateExpression:") {
198 err
199 } else {
200 format!("Invalid UpdateExpression: {err}")
201 }
202}
203
204pub async fn execute<S: StorageBackend>(
205 storage: &S,
206 request: UpdateItemRequest,
207) -> Result<UpdateItemResponse> {
208 execute_inner(storage, request)
212 .await
213 .map_err(crate::validation::envelope_request_validation)
214}
215
216async fn execute_inner<S: StorageBackend>(
217 storage: &S,
218 mut request: UpdateItemRequest,
219) -> Result<UpdateItemResponse> {
220 crate::validation::validate_table_name(&request.table_name)?;
222
223 helpers::validate_no_null_false(&request.key)?;
226 if let Some(ref values) = request.expression_attribute_values {
227 helpers::validate_no_null_false(values)?;
228 }
229 if let Some(ref updates) = request.attribute_updates {
230 for update in updates.values() {
231 if let Some(ref value) = update.value {
232 helpers::validate_no_null_false_value(value)?;
233 }
234 }
235 }
236
237 {
239 let mut non_expr = Vec::new();
240 let mut expr_params = Vec::new();
241 if request.attribute_updates.is_some() {
242 non_expr.push("AttributeUpdates");
243 }
244 if request.expected.is_some() {
245 non_expr.push("Expected");
246 }
247 if request.update_expression.is_some() {
248 expr_params.push("UpdateExpression");
249 }
250 if request.condition_expression.is_some() {
251 expr_params.push("ConditionExpression");
252 }
253 let no_raw_eav: Option<serde_json::Value> = None;
254 let ctx = helpers::ExpressionParamContext {
255 non_expression_params: non_expr,
256 expression_params: expr_params,
257 all_expression_param_names: vec!["UpdateExpression", "ConditionExpression"],
258 expression_attribute_names: &request.expression_attribute_names,
259 expression_attribute_values: &request.expression_attribute_values,
260 expression_attribute_values_raw: &no_raw_eav,
261 };
262 helpers::validate_expression_params(&ctx)
264 .map_err(crate::validation::ClassifiedValidationError::into_tagged)?;
265 }
266
267 crate::validation::validate_key_attribute_values(&request.key)?;
269
270 if request.update_expression.is_none() {
272 if let Some(ref updates) = request.attribute_updates {
273 for (attr_name, update) in updates {
274 let action = update.action.to_uppercase();
275 if update.value.is_none() && action != "DELETE" {
276 return Err(DynoxideError::ValidationException(
277 "One or more parameter values were invalid: \
278 Only DELETE action is allowed when no attribute value is specified"
279 .to_string(),
280 ));
281 }
282 if action == "DELETE" {
283 if let Some(ref val) = update.value {
284 let type_name = match val {
285 AttributeValue::SS(_)
286 | AttributeValue::NS(_)
287 | AttributeValue::BS(_) => None,
288 _ => Some(val.type_name()),
289 };
290 if let Some(tn) = type_name {
291 return Err(DynoxideError::ValidationException(format!(
292 "One or more parameter values were invalid: \
293 DELETE action with value is not supported for the type {tn}"
294 )));
295 }
296 }
297 }
298 if action == "ADD" {
299 if let Some(ref val) = update.value {
300 let allowed = matches!(
301 val,
302 AttributeValue::N(_)
303 | AttributeValue::SS(_)
304 | AttributeValue::NS(_)
305 | AttributeValue::BS(_)
306 | AttributeValue::L(_)
307 );
308 if !allowed {
309 let tn = val.type_name();
310 return Err(DynoxideError::ValidationException(format!(
311 "One or more parameter values were invalid: \
312 ADD action is not supported for the type {tn}"
313 )));
314 }
315 }
316 }
317 let _ = attr_name; }
319 }
320 }
321
322 if request.condition_expression.is_none() && request.update_expression.is_none() {
324 if let Some(ref expected_val) = request.expected {
325 if let Ok(expected) = serde_json::from_value::<
326 HashMap<String, helpers::ExpectedCondition>,
327 >(expected_val.clone())
328 {
329 helpers::validate_expected_conditions(&expected)?;
330 }
331 }
332 }
333
334 if let Some(ref ue) = request.update_expression {
336 if ue.is_empty() {
337 return Err(DynoxideError::ValidationException(
338 "Invalid UpdateExpression: The expression can not be empty;".to_string(),
339 ));
340 }
341 }
342
343 if let Some(ref ce) = request.condition_expression {
345 if ce.is_empty() {
346 return Err(DynoxideError::ValidationException(
347 "Invalid ConditionExpression: The expression can not be empty;".to_string(),
348 ));
349 }
350 }
351
352 if let Some(ref ue) = request.update_expression {
359 let parsed =
360 crate::expressions::update::parse(ue).map_err(DynoxideError::EnvelopedValidation)?;
361
362 let tracker = crate::expressions::TrackedExpressionAttributes::new(
364 &request.expression_attribute_names,
365 &request.expression_attribute_values,
366 );
367 crate::expressions::update::track_references(&parsed, &tracker)
368 .map_err(|e| DynoxideError::EnvelopedValidation(wrap_invalid_update_expression(e)))?;
369
370 if let Some(ref ce) = request.condition_expression {
372 if let Ok(cond_parsed) = crate::expressions::condition::parse(ce) {
373 crate::expressions::condition::track_references(&cond_parsed, &tracker)
374 .map_err(DynoxideError::EnvelopedValidation)?;
375 }
376 }
377
378 tracker.check_unused()?;
380 }
381
382 if let Some(ref ce) = request.condition_expression {
385 let parsed = crate::expressions::condition::parse(ce).map_err(|e| {
386 DynoxideError::EnvelopedValidation(format!("Invalid ConditionExpression: {e}"))
387 })?;
388 crate::expressions::condition::validate_static(
389 &parsed,
390 &request.expression_attribute_values,
391 )
392 .map_err(DynoxideError::EnvelopedValidation)?;
393 crate::expressions::condition::validate_operand_semantics(
394 &parsed,
395 &request.expression_attribute_names,
396 &request.expression_attribute_values,
397 )
398 .map_err(|e| {
399 DynoxideError::EnvelopedValidation(format!("Invalid ConditionExpression: {e}"))
400 })?;
401 }
402
403 if request.condition_expression.is_none() {
405 if let Some(ref expected_val) = request.expected {
406 if let Ok(expected) = serde_json::from_value::<
407 HashMap<String, helpers::ExpectedCondition>,
408 >(expected_val.clone())
409 {
410 if !expected.is_empty() {
411 let (cond_expr, values) = helpers::convert_expected_to_condition(
412 &expected,
413 request.conditional_operator.as_deref(),
414 )?;
415 if !cond_expr.is_empty() {
416 let names = helpers::expected_attr_names(&expected);
417 request.condition_expression = Some(cond_expr);
418 let expr_values = request
419 .expression_attribute_values
420 .get_or_insert_with(HashMap::new);
421 expr_values.extend(values);
422 let expr_names = request
423 .expression_attribute_names
424 .get_or_insert_with(HashMap::new);
425 expr_names.extend(names);
426 }
427 }
428 }
429 }
430 }
431
432 let meta = helpers::require_table_for_item_op(storage, &request.table_name).await?;
433 let key_schema = helpers::parse_key_schema(&meta)?;
434
435 if let Some(ref rv) = request.return_values {
440 let rv_upper = rv.to_uppercase();
441 if !["NONE", "ALL_OLD", "ALL_NEW", "UPDATED_OLD", "UPDATED_NEW"]
442 .contains(&rv_upper.as_str())
443 {
444 return Err(DynoxideError::EnvelopedValidation(format!(
445 "Value '{rv}' at 'returnValues' failed to satisfy constraint: \
446 Member must satisfy enum value set: \
447 [ALL_NEW, UPDATED_OLD, ALL_OLD, NONE, UPDATED_NEW]"
448 )));
449 }
450 }
451
452 helpers::validate_key_only(&request.key, &key_schema)?;
454
455 let (pk, sk) = helpers::extract_key_strings(&request.key, &key_schema)?;
458
459 let legacy_attr_names: Option<Vec<String>> = request
462 .attribute_updates
463 .as_ref()
464 .map(|updates| updates.keys().cloned().collect());
465
466 let tracker = crate::expressions::TrackedExpressionAttributes::without_tracking(
470 &request.expression_attribute_names,
471 &request.expression_attribute_values,
472 );
473
474 let (
480 UpdateWorkResult {
481 old_item,
482 item,
483 item_json,
484 size,
485 },
486 gsi_units,
487 ) = helpers::with_write_transaction(storage, async {
488 let existing_json = storage.get_item(&request.table_name, &pk, &sk).await?;
490 let existing_item: HashMap<String, AttributeValue> = existing_json
491 .as_ref()
492 .and_then(|j| serde_json::from_str(j).ok())
493 .unwrap_or_default();
494
495 if let Some(ref cond_expr) = request.condition_expression {
499 let parsed = crate::expressions::condition::parse(cond_expr)
500 .map_err(DynoxideError::ValidationException)?;
501 let result = crate::expressions::condition::evaluate(&parsed, &existing_item, &tracker)
502 .map_err(DynoxideError::ValidationException)?;
503 if !result {
504 let return_item = if request.return_values_on_condition_check_failure.as_deref()
505 == Some("ALL_OLD")
506 && existing_json.is_some()
507 {
508 Some(existing_item.clone())
509 } else {
510 None
511 };
512 return Err(DynoxideError::ConditionalCheckFailedException(
513 "The conditional request failed".to_string(),
514 return_item,
515 ));
516 }
517 }
518
519 let mut item = existing_item;
522 if existing_json.is_none() {
523 for (k, v) in &request.key {
524 item.insert(k.clone(), v.clone());
525 }
526 }
527
528 let old_item = item.clone();
530
531 if let Some(ref update_expr) = request.update_expression {
533 let parsed = crate::expressions::update::parse(update_expr)
534 .map_err(DynoxideError::ValidationException)?;
535
536 for action in &parsed.set_actions {
539 validate_not_key_attr(
540 action.path.first(),
541 &key_schema,
542 &request.expression_attribute_names,
543 )?;
544 }
545
546 for path in &parsed.remove_actions {
548 validate_not_key_attr(
549 path.first(),
550 &key_schema,
551 &request.expression_attribute_names,
552 )?;
553 }
554
555 for action in &parsed.add_actions {
557 validate_not_key_attr(
558 action.path.first(),
559 &key_schema,
560 &request.expression_attribute_names,
561 )?;
562 }
563
564 for action in &parsed.delete_actions {
566 validate_not_key_attr(
567 action.path.first(),
568 &key_schema,
569 &request.expression_attribute_names,
570 )?;
571 }
572
573 crate::expressions::update::apply(&mut item, &parsed, &tracker)
574 .map_err(DynoxideError::ValidationException)?;
575 }
576
577 if request.update_expression.is_none() {
579 if let Some(ref updates) = request.attribute_updates {
580 apply_attribute_updates(&mut item, updates, &key_schema)?;
581 }
582 }
583
584 crate::validation::validate_item_attribute_values(&item)?;
594 crate::validation::normalize_item_sets(&mut item);
595
596 helpers::validate_updated_index_keys(&old_item, &item, &meta)?;
599
600 let size = types::item_size(&item);
602 if size > types::MAX_ITEM_SIZE {
603 return Err(DynoxideError::ValidationException(
604 "Item size to update has exceeded the maximum allowed size".to_string(),
605 ));
606 }
607
608 let item_json = serde_json::to_string(&item)
610 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
611 let hash_prefix = request
612 .key
613 .get(&key_schema.partition_key)
614 .map(crate::storage::compute_hash_prefix)
615 .unwrap_or_default();
616 storage
617 .put_item_with_hash(
618 &request.table_name,
619 &pk,
620 &sk,
621 &item_json,
622 size,
623 &hash_prefix,
624 )
625 .await?;
626
627 let gsi_units = super::gsi::maintain_gsis_after_write(
629 storage,
630 &request.table_name,
631 &meta,
632 &pk,
633 &sk,
634 &item,
635 &key_schema.partition_key,
636 key_schema.sort_key.as_deref(),
637 )
638 .await?;
639
640 super::lsi::maintain_lsis_after_write(
642 storage,
643 &request.table_name,
644 &meta,
645 &pk,
646 &sk,
647 &item,
648 &key_schema.partition_key,
649 key_schema.sort_key.as_deref(),
650 )
651 .await?;
652
653 let old_for_stream = if existing_json.is_some() {
655 Some(&old_item)
656 } else {
657 None
658 };
659 crate::streams::record_stream_event(storage, &meta, old_for_stream, Some(&item)).await?;
660
661 Ok((
662 UpdateWorkResult {
663 old_item,
664 item,
665 item_json,
666 size,
667 },
668 gsi_units,
669 ))
670 })
671 .await?;
672
673 let return_values = request.return_values.as_deref().unwrap_or("NONE");
675 let attributes = match return_values.to_uppercase().as_str() {
676 "ALL_OLD" => Some(old_item),
677 "ALL_NEW" => Some(item),
678 "UPDATED_OLD" => {
679 if let Some(ref update_expr) = request.update_expression {
680 let parsed = crate::expressions::update::parse(update_expr)
682 .map_err(DynoxideError::ValidationException)?;
683 omit_if_empty(extract_updated_attrs(
684 &old_item,
685 &parsed,
686 &request.expression_attribute_names,
687 ))
688 } else {
689 legacy_attr_names
691 .as_ref()
692 .map(|names| extract_named_attrs(&old_item, names))
693 .and_then(omit_if_empty)
694 }
695 }
696 "UPDATED_NEW" => {
697 if let Some(ref update_expr) = request.update_expression {
698 let parsed = crate::expressions::update::parse(update_expr)
700 .map_err(DynoxideError::ValidationException)?;
701 let new_item: HashMap<String, AttributeValue> = serde_json::from_str(&item_json)
702 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
703 omit_if_empty(extract_updated_attrs(
704 &new_item,
705 &parsed,
706 &request.expression_attribute_names,
707 ))
708 } else {
709 legacy_attr_names
711 .as_ref()
712 .map(|names| {
713 let new_item: HashMap<String, AttributeValue> =
714 serde_json::from_str(&item_json).unwrap_or_default();
715 extract_named_attrs(&new_item, names)
716 })
717 .and_then(omit_if_empty)
718 }
719 }
720 _ => None, };
722
723 let pk_value = request.key.get(&key_schema.partition_key).cloned();
725 let item_collection_metrics = helpers::build_item_collection_metrics(
726 storage,
727 &meta,
728 &request.table_name,
729 &pk,
730 &key_schema.partition_key,
731 pk_value
732 .as_ref()
733 .unwrap_or(&AttributeValue::S(String::new())),
734 &request.return_item_collection_metrics,
735 )
736 .await?;
737
738 let consumed_capacity = types::consumed_capacity_with_indexes(
739 &request.table_name,
740 types::write_capacity_units(size),
741 &gsi_units,
742 &request.return_consumed_capacity,
743 );
744
745 Ok(UpdateItemResponse {
746 attributes,
747 consumed_capacity,
748 item_collection_metrics,
749 })
750}
751
752fn apply_attribute_updates(
759 item: &mut HashMap<String, AttributeValue>,
760 updates: &HashMap<String, AttributeValueUpdate>,
761 key_schema: &helpers::KeySchema,
762) -> Result<()> {
763 for (attr_name, update) in updates {
764 if attr_name == &key_schema.partition_key
766 || key_schema
767 .sort_key
768 .as_ref()
769 .is_some_and(|sk| sk == attr_name)
770 {
771 return Err(DynoxideError::ValidationException(format!(
772 "One or more parameter values were invalid: \
773 Cannot update attribute {attr_name}. This attribute is part of the key"
774 )));
775 }
776
777 let action = update.action.to_uppercase();
778 match action.as_str() {
779 "PUT" => {
780 if let Some(ref value) = update.value {
781 item.insert(attr_name.clone(), value.clone());
782 }
783 }
784 "ADD" => {
785 if let Some(ref add_val) = update.value {
786 let path = vec![crate::expressions::PathElement::Attribute(
787 attr_name.clone(),
788 )];
789 crate::expressions::update::apply_add_public(item, &path, add_val)
790 .map_err(DynoxideError::ValidationException)?;
791 }
792 }
793 "DELETE" => {
794 if let Some(ref del_val) = update.value {
795 let path = vec![crate::expressions::PathElement::Attribute(
797 attr_name.clone(),
798 )];
799 crate::expressions::update::apply_delete_public(item, &path, del_val)
800 .map_err(DynoxideError::ValidationException)?;
801 } else {
802 item.remove(attr_name);
804 }
805 }
806 _ => {
807 return Err(DynoxideError::ValidationException(format!(
808 "1 validation error detected: Value '{action}' at 'attributeUpdates.{attr_name}.member.action' \
809 failed to satisfy constraint: Member must satisfy enum value set: [ADD, PUT, DELETE]"
810 )));
811 }
812 }
813 }
814 Ok(())
815}
816
817fn extract_updated_attrs(
826 item: &HashMap<String, AttributeValue>,
827 expr: &crate::expressions::update::UpdateExpr,
828 attr_names: &Option<HashMap<String, String>>,
829) -> HashMap<String, AttributeValue> {
830 use crate::expressions::{PathElement, resolve_path, resolve_path_elements};
831
832 let no_values: Option<HashMap<String, AttributeValue>> = None;
833 let tracker =
834 crate::expressions::TrackedExpressionAttributes::without_tracking(attr_names, &no_values);
835
836 let mut paths: Vec<&[PathElement]> = Vec::new();
838 paths.extend(expr.set_actions.iter().map(|a| a.path.as_slice()));
839 paths.extend(expr.remove_actions.iter().map(|p| p.as_slice()));
840 paths.extend(expr.add_actions.iter().map(|a| a.path.as_slice()));
841 paths.extend(expr.delete_actions.iter().map(|a| a.path.as_slice()));
842
843 let mut result = HashMap::new();
844 for path in paths {
845 let Ok(resolved) = resolve_path_elements(path, &tracker) else {
846 continue;
847 };
848
849 if resolved.iter().any(|e| matches!(e, PathElement::Index(_))) {
856 if let Some(PathElement::Attribute(top)) = resolved.first() {
857 if let Some(val) = item.get(top) {
858 result.insert(top.clone(), val.clone());
859 }
860 }
861 continue;
862 }
863
864 if let Some(val) = resolve_path(item, &resolved) {
865 crate::expressions::projection::insert_at_path(&mut result, &resolved, val);
866 }
867 }
868
869 result
870}
871
872fn omit_if_empty(map: HashMap<String, AttributeValue>) -> Option<HashMap<String, AttributeValue>> {
879 if map.is_empty() { None } else { Some(map) }
880}
881
882fn extract_named_attrs(
884 item: &HashMap<String, AttributeValue>,
885 attr_names: &[String],
886) -> HashMap<String, AttributeValue> {
887 let mut result = HashMap::new();
888 for name in attr_names {
889 if let Some(val) = item.get(name) {
890 result.insert(name.clone(), val.clone());
891 }
892 }
893 result
894}
895
896fn validate_not_key_attr(
898 first_element: Option<&crate::expressions::PathElement>,
899 key_schema: &helpers::KeySchema,
900 expression_attribute_names: &Option<HashMap<String, String>>,
901) -> crate::errors::Result<()> {
902 if let Some(crate::expressions::PathElement::Attribute(name)) = first_element {
903 let resolved_name = if name.starts_with('#') {
904 crate::expressions::resolve_name(name, expression_attribute_names)
905 .map_err(DynoxideError::ValidationException)?
906 } else {
907 name.clone()
908 };
909 if resolved_name == key_schema.partition_key
910 || key_schema
911 .sort_key
912 .as_ref()
913 .is_some_and(|sk| sk == &resolved_name)
914 {
915 return Err(DynoxideError::ValidationException(format!(
916 "One or more parameter values were invalid: Cannot update attribute {resolved_name}. This attribute is part of the key"
917 )));
918 }
919 }
920 Ok(())
921}
922
923#[cfg(test)]
924mod tests {
925 use crate::actions::{create_table, put_item, update_item};
926 use crate::storage::Storage;
927 use crate::storage_backend::StorageBackend;
928
929 #[test]
930 fn update_item_stops_at_first_invalid_enum() {
931 let err = serde_json::from_value::<super::UpdateItemRequest>(serde_json::json!({
934 "TableName": "_conformance_valid_table_name",
935 "Key": {"pk": {"S": "test"}},
936 "ReturnValues": "INVALID",
937 "ReturnConsumedCapacity": "INVALID"
938 }))
939 .unwrap_err()
940 .to_string();
941 assert!(err.contains("1 validation error detected"), "got: {err}");
942 assert!(err.contains("enum value set"), "got: {err}");
943 assert!(err.contains("returnValues"), "got: {err}");
944 assert!(!err.contains("returnConsumedCapacity"), "got: {err}");
945 }
946
947 #[test]
948 fn update_item_empty_table_name_reports_only_table_name() {
949 let err = serde_json::from_value::<super::UpdateItemRequest>(serde_json::json!({
950 "TableName": "",
951 "Key": {}
952 }))
953 .unwrap_err()
954 .to_string();
955 assert!(err.contains("1 validation error detected"), "got: {err}");
956 assert!(err.to_lowercase().contains("tablename"), "got: {err}");
957 }
958
959 #[test]
962 fn update_item_rolls_back_base_write_when_gsi_fan_out_fails() {
963 let storage = Storage::memory().unwrap();
964
965 let create = serde_json::from_value(serde_json::json!({
966 "TableName": "Orders",
967 "KeySchema": [{"AttributeName": "UserId", "KeyType": "HASH"}],
968 "AttributeDefinitions": [
969 {"AttributeName": "UserId", "AttributeType": "S"},
970 {"AttributeName": "Status", "AttributeType": "S"},
971 {"AttributeName": "Priority", "AttributeType": "S"}
972 ],
973 "GlobalSecondaryIndexes": [
974 {"IndexName": "StatusIndex", "KeySchema": [{"AttributeName": "Status", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}},
975 {"IndexName": "PriorityIndex", "KeySchema": [{"AttributeName": "Priority", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}}
976 ]
977 }))
978 .unwrap();
979 pollster::block_on(create_table::execute(&storage, create)).unwrap();
980
981 let put = serde_json::from_value(serde_json::json!({
982 "TableName": "Orders",
983 "Item": {"UserId": {"S": "u1"}, "Status": {"S": "SHIPPED"}, "Priority": {"S": "HIGH"}, "Note": {"S": "before"}}
984 }))
985 .unwrap();
986 pollster::block_on(put_item::execute(&storage, put)).unwrap();
987
988 storage.drop_gsi_table("Orders", "PriorityIndex").unwrap();
990
991 let update = serde_json::from_value(serde_json::json!({
992 "TableName": "Orders",
993 "Key": {"UserId": {"S": "u1"}},
994 "UpdateExpression": "SET Note = :n",
995 "ExpressionAttributeValues": {":n": {"S": "after"}}
996 }))
997 .unwrap();
998 let res = pollster::block_on(update_item::execute(&storage, update));
999 assert!(
1000 res.is_err(),
1001 "a mid-fan-out failure must surface as an error"
1002 );
1003
1004 let rows = pollster::block_on(<Storage as StorageBackend>::scan_items(
1007 &storage,
1008 "Orders",
1009 &Default::default(),
1010 ))
1011 .unwrap();
1012 assert_eq!(
1013 rows.len(),
1014 1,
1015 "the item must still be present after rollback"
1016 );
1017 let raw = &rows[0].2;
1018 assert!(
1019 raw.contains("\"before\"") && !raw.contains("\"after\""),
1020 "update must roll back when fan-out fails, leaving the original value: {raw}"
1021 );
1022 }
1023}