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 mut request: UpdateItemRequest,
207) -> Result<UpdateItemResponse> {
208 crate::validation::validate_table_name(&request.table_name)?;
210
211 {
213 let mut non_expr = Vec::new();
214 let mut expr_params = Vec::new();
215 if request.attribute_updates.is_some() {
216 non_expr.push("AttributeUpdates");
217 }
218 if request.expected.is_some() {
219 non_expr.push("Expected");
220 }
221 if request.update_expression.is_some() {
222 expr_params.push("UpdateExpression");
223 }
224 if request.condition_expression.is_some() {
225 expr_params.push("ConditionExpression");
226 }
227 let no_raw_eav: Option<serde_json::Value> = None;
228 let ctx = helpers::ExpressionParamContext {
229 non_expression_params: non_expr,
230 expression_params: expr_params,
231 all_expression_param_names: vec!["UpdateExpression", "ConditionExpression"],
232 expression_attribute_names: &request.expression_attribute_names,
233 expression_attribute_values: &request.expression_attribute_values,
234 expression_attribute_values_raw: &no_raw_eav,
235 };
236 helpers::validate_expression_params(&ctx)?;
237 }
238
239 crate::validation::validate_key_attribute_values(&request.key)?;
241
242 if request.update_expression.is_none() {
244 if let Some(ref updates) = request.attribute_updates {
245 for (attr_name, update) in updates {
246 let action = update.action.to_uppercase();
247 if update.value.is_none() && action != "DELETE" {
248 return Err(DynoxideError::ValidationException(
249 "One or more parameter values were invalid: \
250 Only DELETE action is allowed when no attribute value is specified"
251 .to_string(),
252 ));
253 }
254 if action == "DELETE" {
255 if let Some(ref val) = update.value {
256 let type_name = match val {
257 AttributeValue::SS(_)
258 | AttributeValue::NS(_)
259 | AttributeValue::BS(_) => None,
260 _ => Some(val.type_name()),
261 };
262 if let Some(tn) = type_name {
263 return Err(DynoxideError::ValidationException(format!(
264 "One or more parameter values were invalid: \
265 DELETE action with value is not supported for the type {tn}"
266 )));
267 }
268 }
269 }
270 if action == "ADD" {
271 if let Some(ref val) = update.value {
272 let allowed = matches!(
273 val,
274 AttributeValue::N(_)
275 | AttributeValue::SS(_)
276 | AttributeValue::NS(_)
277 | AttributeValue::BS(_)
278 | AttributeValue::L(_)
279 );
280 if !allowed {
281 let tn = val.type_name();
282 return Err(DynoxideError::ValidationException(format!(
283 "One or more parameter values were invalid: \
284 ADD action is not supported for the type {tn}"
285 )));
286 }
287 }
288 }
289 let _ = attr_name; }
291 }
292 }
293
294 if request.condition_expression.is_none() && request.update_expression.is_none() {
296 if let Some(ref expected_val) = request.expected {
297 if let Ok(expected) = serde_json::from_value::<
298 HashMap<String, helpers::ExpectedCondition>,
299 >(expected_val.clone())
300 {
301 helpers::validate_expected_conditions(&expected)?;
302 }
303 }
304 }
305
306 if let Some(ref ue) = request.update_expression {
308 if ue.is_empty() {
309 return Err(DynoxideError::ValidationException(
310 "Invalid UpdateExpression: The expression can not be empty;".to_string(),
311 ));
312 }
313 }
314
315 if let Some(ref ce) = request.condition_expression {
317 if ce.is_empty() {
318 return Err(DynoxideError::ValidationException(
319 "Invalid ConditionExpression: The expression can not be empty;".to_string(),
320 ));
321 }
322 }
323
324 if let Some(ref ue) = request.update_expression {
328 let parsed =
329 crate::expressions::update::parse(ue).map_err(DynoxideError::ValidationException)?;
330
331 let tracker = crate::expressions::TrackedExpressionAttributes::new(
333 &request.expression_attribute_names,
334 &request.expression_attribute_values,
335 );
336 crate::expressions::update::track_references(&parsed, &tracker)
337 .map_err(|e| DynoxideError::ValidationException(wrap_invalid_update_expression(e)))?;
338
339 if let Some(ref ce) = request.condition_expression {
341 if let Ok(cond_parsed) = crate::expressions::condition::parse(ce) {
342 crate::expressions::condition::track_references(&cond_parsed, &tracker)
343 .map_err(DynoxideError::ValidationException)?;
344 }
345 }
346
347 tracker.check_unused()?;
349 }
350
351 if let Some(ref ce) = request.condition_expression {
353 let parsed = crate::expressions::condition::parse(ce).map_err(|e| {
354 DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
355 })?;
356 crate::expressions::condition::validate_static(
357 &parsed,
358 &request.expression_attribute_values,
359 )
360 .map_err(DynoxideError::ValidationException)?;
361 crate::expressions::condition::validate_operand_semantics(
362 &parsed,
363 &request.expression_attribute_names,
364 &request.expression_attribute_values,
365 )
366 .map_err(|e| {
367 DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
368 })?;
369 }
370
371 if request.condition_expression.is_none() {
373 if let Some(ref expected_val) = request.expected {
374 if let Ok(expected) = serde_json::from_value::<
375 HashMap<String, helpers::ExpectedCondition>,
376 >(expected_val.clone())
377 {
378 if !expected.is_empty() {
379 let (cond_expr, values) = helpers::convert_expected_to_condition(
380 &expected,
381 request.conditional_operator.as_deref(),
382 )?;
383 if !cond_expr.is_empty() {
384 let names = helpers::expected_attr_names(&expected);
385 request.condition_expression = Some(cond_expr);
386 let expr_values = request
387 .expression_attribute_values
388 .get_or_insert_with(HashMap::new);
389 expr_values.extend(values);
390 let expr_names = request
391 .expression_attribute_names
392 .get_or_insert_with(HashMap::new);
393 expr_names.extend(names);
394 }
395 }
396 }
397 }
398 }
399
400 let meta = helpers::require_table_for_item_op(storage, &request.table_name).await?;
401 let key_schema = helpers::parse_key_schema(&meta)?;
402
403 if let Some(ref rv) = request.return_values {
405 let rv_upper = rv.to_uppercase();
406 if !["NONE", "ALL_OLD", "ALL_NEW", "UPDATED_OLD", "UPDATED_NEW"]
407 .contains(&rv_upper.as_str())
408 {
409 return Err(DynoxideError::ValidationException(format!(
410 "1 validation error detected: Value '{rv}' at 'returnValues' failed to satisfy constraint: \
411 Member must satisfy enum value set: [ALL_NEW, ALL_OLD, NONE, UPDATED_NEW, UPDATED_OLD]"
412 )));
413 }
414 }
415
416 helpers::validate_key_only(&request.key, &key_schema)?;
418
419 let (pk, sk) = helpers::extract_key_strings(&request.key, &key_schema)?;
422
423 let legacy_attr_names: Option<Vec<String>> = request
426 .attribute_updates
427 .as_ref()
428 .map(|updates| updates.keys().cloned().collect());
429
430 let tracker = crate::expressions::TrackedExpressionAttributes::without_tracking(
434 &request.expression_attribute_names,
435 &request.expression_attribute_values,
436 );
437
438 let (
444 UpdateWorkResult {
445 old_item,
446 item,
447 item_json,
448 size,
449 },
450 gsi_units,
451 ) = helpers::with_write_transaction(storage, async {
452 let existing_json = storage.get_item(&request.table_name, &pk, &sk).await?;
454 let existing_item: HashMap<String, AttributeValue> = existing_json
455 .as_ref()
456 .and_then(|j| serde_json::from_str(j).ok())
457 .unwrap_or_default();
458
459 if let Some(ref cond_expr) = request.condition_expression {
463 let parsed = crate::expressions::condition::parse(cond_expr)
464 .map_err(DynoxideError::ValidationException)?;
465 let result = crate::expressions::condition::evaluate(&parsed, &existing_item, &tracker)
466 .map_err(DynoxideError::ValidationException)?;
467 if !result {
468 let return_item = if request.return_values_on_condition_check_failure.as_deref()
469 == Some("ALL_OLD")
470 && existing_json.is_some()
471 {
472 Some(existing_item.clone())
473 } else {
474 None
475 };
476 return Err(DynoxideError::ConditionalCheckFailedException(
477 "The conditional request failed".to_string(),
478 return_item,
479 ));
480 }
481 }
482
483 let mut item = existing_item;
486 if existing_json.is_none() {
487 for (k, v) in &request.key {
488 item.insert(k.clone(), v.clone());
489 }
490 }
491
492 let old_item = item.clone();
494
495 if let Some(ref update_expr) = request.update_expression {
497 let parsed = crate::expressions::update::parse(update_expr)
498 .map_err(DynoxideError::ValidationException)?;
499
500 for action in &parsed.set_actions {
503 validate_not_key_attr(
504 action.path.first(),
505 &key_schema,
506 &request.expression_attribute_names,
507 )?;
508 }
509
510 for path in &parsed.remove_actions {
512 validate_not_key_attr(
513 path.first(),
514 &key_schema,
515 &request.expression_attribute_names,
516 )?;
517 }
518
519 for action in &parsed.add_actions {
521 validate_not_key_attr(
522 action.path.first(),
523 &key_schema,
524 &request.expression_attribute_names,
525 )?;
526 }
527
528 for action in &parsed.delete_actions {
530 validate_not_key_attr(
531 action.path.first(),
532 &key_schema,
533 &request.expression_attribute_names,
534 )?;
535 }
536
537 crate::expressions::update::apply(&mut item, &parsed, &tracker)
538 .map_err(DynoxideError::ValidationException)?;
539 }
540
541 if request.update_expression.is_none() {
543 if let Some(ref updates) = request.attribute_updates {
544 apply_attribute_updates(&mut item, updates, &key_schema)?;
545 }
546 }
547
548 crate::validation::validate_item_attribute_values(&item)?;
554 crate::validation::normalize_item_sets(&mut item);
555
556 helpers::validate_updated_index_keys(&old_item, &item, &meta)?;
559
560 let size = types::item_size(&item);
562 if size > types::MAX_ITEM_SIZE {
563 return Err(DynoxideError::ValidationException(
564 "Item size to update has exceeded the maximum allowed size".to_string(),
565 ));
566 }
567
568 let item_json = serde_json::to_string(&item)
570 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
571 let hash_prefix = request
572 .key
573 .get(&key_schema.partition_key)
574 .map(crate::storage::compute_hash_prefix)
575 .unwrap_or_default();
576 storage
577 .put_item_with_hash(
578 &request.table_name,
579 &pk,
580 &sk,
581 &item_json,
582 size,
583 &hash_prefix,
584 )
585 .await?;
586
587 let gsi_units = super::gsi::maintain_gsis_after_write(
589 storage,
590 &request.table_name,
591 &meta,
592 &pk,
593 &sk,
594 &item,
595 &key_schema.partition_key,
596 key_schema.sort_key.as_deref(),
597 )
598 .await?;
599
600 super::lsi::maintain_lsis_after_write(
602 storage,
603 &request.table_name,
604 &meta,
605 &pk,
606 &sk,
607 &item,
608 &key_schema.partition_key,
609 key_schema.sort_key.as_deref(),
610 )
611 .await?;
612
613 let old_for_stream = if existing_json.is_some() {
615 Some(&old_item)
616 } else {
617 None
618 };
619 crate::streams::record_stream_event(storage, &meta, old_for_stream, Some(&item)).await?;
620
621 Ok((
622 UpdateWorkResult {
623 old_item,
624 item,
625 item_json,
626 size,
627 },
628 gsi_units,
629 ))
630 })
631 .await?;
632
633 let return_values = request.return_values.as_deref().unwrap_or("NONE");
635 let attributes = match return_values.to_uppercase().as_str() {
636 "ALL_OLD" => Some(old_item),
637 "ALL_NEW" => Some(item),
638 "UPDATED_OLD" => {
639 if let Some(ref update_expr) = request.update_expression {
640 let parsed = crate::expressions::update::parse(update_expr)
642 .map_err(DynoxideError::ValidationException)?;
643 omit_if_empty(extract_updated_attrs(
644 &old_item,
645 &parsed,
646 &request.expression_attribute_names,
647 ))
648 } else {
649 legacy_attr_names
651 .as_ref()
652 .map(|names| extract_named_attrs(&old_item, names))
653 .and_then(omit_if_empty)
654 }
655 }
656 "UPDATED_NEW" => {
657 if let Some(ref update_expr) = request.update_expression {
658 let parsed = crate::expressions::update::parse(update_expr)
660 .map_err(DynoxideError::ValidationException)?;
661 let new_item: HashMap<String, AttributeValue> = serde_json::from_str(&item_json)
662 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
663 omit_if_empty(extract_updated_attrs(
664 &new_item,
665 &parsed,
666 &request.expression_attribute_names,
667 ))
668 } else {
669 legacy_attr_names
671 .as_ref()
672 .map(|names| {
673 let new_item: HashMap<String, AttributeValue> =
674 serde_json::from_str(&item_json).unwrap_or_default();
675 extract_named_attrs(&new_item, names)
676 })
677 .and_then(omit_if_empty)
678 }
679 }
680 _ => None, };
682
683 let pk_value = request.key.get(&key_schema.partition_key).cloned();
685 let item_collection_metrics = helpers::build_item_collection_metrics(
686 storage,
687 &meta,
688 &request.table_name,
689 &pk,
690 &key_schema.partition_key,
691 pk_value
692 .as_ref()
693 .unwrap_or(&AttributeValue::S(String::new())),
694 &request.return_item_collection_metrics,
695 )
696 .await?;
697
698 let consumed_capacity = types::consumed_capacity_with_indexes(
699 &request.table_name,
700 types::write_capacity_units(size),
701 &gsi_units,
702 &request.return_consumed_capacity,
703 );
704
705 Ok(UpdateItemResponse {
706 attributes,
707 consumed_capacity,
708 item_collection_metrics,
709 })
710}
711
712fn apply_attribute_updates(
719 item: &mut HashMap<String, AttributeValue>,
720 updates: &HashMap<String, AttributeValueUpdate>,
721 key_schema: &helpers::KeySchema,
722) -> Result<()> {
723 for (attr_name, update) in updates {
724 if attr_name == &key_schema.partition_key
726 || key_schema
727 .sort_key
728 .as_ref()
729 .is_some_and(|sk| sk == attr_name)
730 {
731 return Err(DynoxideError::ValidationException(format!(
732 "One or more parameter values were invalid: \
733 Cannot update attribute {attr_name}. This attribute is part of the key"
734 )));
735 }
736
737 let action = update.action.to_uppercase();
738 match action.as_str() {
739 "PUT" => {
740 if let Some(ref value) = update.value {
741 item.insert(attr_name.clone(), value.clone());
742 }
743 }
744 "ADD" => {
745 if let Some(ref add_val) = update.value {
746 let path = vec![crate::expressions::PathElement::Attribute(
747 attr_name.clone(),
748 )];
749 crate::expressions::update::apply_add_public(item, &path, add_val)
750 .map_err(DynoxideError::ValidationException)?;
751 }
752 }
753 "DELETE" => {
754 if let Some(ref del_val) = update.value {
755 let path = vec![crate::expressions::PathElement::Attribute(
757 attr_name.clone(),
758 )];
759 crate::expressions::update::apply_delete_public(item, &path, del_val)
760 .map_err(DynoxideError::ValidationException)?;
761 } else {
762 item.remove(attr_name);
764 }
765 }
766 _ => {
767 return Err(DynoxideError::ValidationException(format!(
768 "1 validation error detected: Value '{action}' at 'attributeUpdates.{attr_name}.member.action' \
769 failed to satisfy constraint: Member must satisfy enum value set: [ADD, PUT, DELETE]"
770 )));
771 }
772 }
773 }
774 Ok(())
775}
776
777fn extract_updated_attrs(
786 item: &HashMap<String, AttributeValue>,
787 expr: &crate::expressions::update::UpdateExpr,
788 attr_names: &Option<HashMap<String, String>>,
789) -> HashMap<String, AttributeValue> {
790 use crate::expressions::{PathElement, resolve_path, resolve_path_elements};
791
792 let no_values: Option<HashMap<String, AttributeValue>> = None;
793 let tracker =
794 crate::expressions::TrackedExpressionAttributes::without_tracking(attr_names, &no_values);
795
796 let mut paths: Vec<&[PathElement]> = Vec::new();
798 paths.extend(expr.set_actions.iter().map(|a| a.path.as_slice()));
799 paths.extend(expr.remove_actions.iter().map(|p| p.as_slice()));
800 paths.extend(expr.add_actions.iter().map(|a| a.path.as_slice()));
801 paths.extend(expr.delete_actions.iter().map(|a| a.path.as_slice()));
802
803 let mut result = HashMap::new();
804 for path in paths {
805 let Ok(resolved) = resolve_path_elements(path, &tracker) else {
806 continue;
807 };
808
809 if resolved.iter().any(|e| matches!(e, PathElement::Index(_))) {
816 if let Some(PathElement::Attribute(top)) = resolved.first() {
817 if let Some(val) = item.get(top) {
818 result.insert(top.clone(), val.clone());
819 }
820 }
821 continue;
822 }
823
824 if let Some(val) = resolve_path(item, &resolved) {
825 crate::expressions::projection::insert_at_path(&mut result, &resolved, val);
826 }
827 }
828
829 result
830}
831
832fn omit_if_empty(map: HashMap<String, AttributeValue>) -> Option<HashMap<String, AttributeValue>> {
839 if map.is_empty() { None } else { Some(map) }
840}
841
842fn extract_named_attrs(
844 item: &HashMap<String, AttributeValue>,
845 attr_names: &[String],
846) -> HashMap<String, AttributeValue> {
847 let mut result = HashMap::new();
848 for name in attr_names {
849 if let Some(val) = item.get(name) {
850 result.insert(name.clone(), val.clone());
851 }
852 }
853 result
854}
855
856fn validate_not_key_attr(
858 first_element: Option<&crate::expressions::PathElement>,
859 key_schema: &helpers::KeySchema,
860 expression_attribute_names: &Option<HashMap<String, String>>,
861) -> crate::errors::Result<()> {
862 if let Some(crate::expressions::PathElement::Attribute(name)) = first_element {
863 let resolved_name = if name.starts_with('#') {
864 crate::expressions::resolve_name(name, expression_attribute_names)
865 .map_err(DynoxideError::ValidationException)?
866 } else {
867 name.clone()
868 };
869 if resolved_name == key_schema.partition_key
870 || key_schema
871 .sort_key
872 .as_ref()
873 .is_some_and(|sk| sk == &resolved_name)
874 {
875 return Err(DynoxideError::ValidationException(format!(
876 "One or more parameter values were invalid: Cannot update attribute {resolved_name}. This attribute is part of the key"
877 )));
878 }
879 }
880 Ok(())
881}
882
883#[cfg(test)]
884mod tests {
885 use crate::actions::{create_table, put_item, update_item};
886 use crate::storage::Storage;
887 use crate::storage_backend::StorageBackend;
888
889 #[test]
890 fn update_item_stops_at_first_invalid_enum() {
891 let err = serde_json::from_value::<super::UpdateItemRequest>(serde_json::json!({
894 "TableName": "_conformance_valid_table_name",
895 "Key": {"pk": {"S": "test"}},
896 "ReturnValues": "INVALID",
897 "ReturnConsumedCapacity": "INVALID"
898 }))
899 .unwrap_err()
900 .to_string();
901 assert!(err.contains("1 validation error detected"), "got: {err}");
902 assert!(err.contains("enum value set"), "got: {err}");
903 assert!(err.contains("returnValues"), "got: {err}");
904 assert!(!err.contains("returnConsumedCapacity"), "got: {err}");
905 }
906
907 #[test]
908 fn update_item_empty_table_name_reports_only_table_name() {
909 let err = serde_json::from_value::<super::UpdateItemRequest>(serde_json::json!({
910 "TableName": "",
911 "Key": {}
912 }))
913 .unwrap_err()
914 .to_string();
915 assert!(err.contains("1 validation error detected"), "got: {err}");
916 assert!(err.to_lowercase().contains("tablename"), "got: {err}");
917 }
918
919 #[test]
922 fn update_item_rolls_back_base_write_when_gsi_fan_out_fails() {
923 let storage = Storage::memory().unwrap();
924
925 let create = serde_json::from_value(serde_json::json!({
926 "TableName": "Orders",
927 "KeySchema": [{"AttributeName": "UserId", "KeyType": "HASH"}],
928 "AttributeDefinitions": [
929 {"AttributeName": "UserId", "AttributeType": "S"},
930 {"AttributeName": "Status", "AttributeType": "S"},
931 {"AttributeName": "Priority", "AttributeType": "S"}
932 ],
933 "GlobalSecondaryIndexes": [
934 {"IndexName": "StatusIndex", "KeySchema": [{"AttributeName": "Status", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}},
935 {"IndexName": "PriorityIndex", "KeySchema": [{"AttributeName": "Priority", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}}
936 ]
937 }))
938 .unwrap();
939 pollster::block_on(create_table::execute(&storage, create)).unwrap();
940
941 let put = serde_json::from_value(serde_json::json!({
942 "TableName": "Orders",
943 "Item": {"UserId": {"S": "u1"}, "Status": {"S": "SHIPPED"}, "Priority": {"S": "HIGH"}, "Note": {"S": "before"}}
944 }))
945 .unwrap();
946 pollster::block_on(put_item::execute(&storage, put)).unwrap();
947
948 storage.drop_gsi_table("Orders", "PriorityIndex").unwrap();
950
951 let update = serde_json::from_value(serde_json::json!({
952 "TableName": "Orders",
953 "Key": {"UserId": {"S": "u1"}},
954 "UpdateExpression": "SET Note = :n",
955 "ExpressionAttributeValues": {":n": {"S": "after"}}
956 }))
957 .unwrap();
958 let res = pollster::block_on(update_item::execute(&storage, update));
959 assert!(
960 res.is_err(),
961 "a mid-fan-out failure must surface as an error"
962 );
963
964 let rows = pollster::block_on(<Storage as StorageBackend>::scan_items(
967 &storage,
968 "Orders",
969 &Default::default(),
970 ))
971 .unwrap();
972 assert_eq!(
973 rows.len(),
974 1,
975 "the item must still be present after rollback"
976 );
977 let raw = &rows[0].2;
978 assert!(
979 raw.contains("\"before\"") && !raw.contains("\"after\""),
980 "update must roll back when fan-out fails, leaving the original value: {raw}"
981 );
982 }
983}