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
64impl<'de> serde::Deserialize<'de> for UpdateItemRequest {
65 fn deserialize<D: serde::Deserializer<'de>>(
66 deserializer: D,
67 ) -> std::result::Result<Self, D::Error> {
68 let raw = UpdateItemRequestRaw::deserialize(deserializer)?;
69 use crate::validation::{
70 TableNameContext, format_validation_errors, table_name_constraint_errors,
71 };
72
73 let mut errors = Vec::new();
74
75 errors.extend(table_name_constraint_errors(
77 raw.table_name.as_deref(),
78 TableNameContext::ReadWrite,
79 ));
80 let table_name = raw.table_name.unwrap_or_default();
81
82 if raw.key.is_none() {
84 errors.push(
85 "Value null at 'key' failed to satisfy constraint: \
86 Member must not be null"
87 .to_string(),
88 );
89 }
90
91 if let Some(ref rcc) = raw.return_consumed_capacity {
93 if !["INDEXES", "TOTAL", "NONE"].contains(&rcc.as_str()) {
94 errors.push(format!(
95 "Value '{}' at 'returnConsumedCapacity' failed to satisfy constraint: \
96 Member must satisfy enum value set: [INDEXES, TOTAL, NONE]",
97 rcc
98 ));
99 }
100 }
101
102 if let Some(ref rv) = raw.return_values {
104 if !["ALL_NEW", "UPDATED_OLD", "ALL_OLD", "NONE", "UPDATED_NEW"].contains(&rv.as_str())
105 {
106 errors.push(format!(
107 "Value '{}' at 'returnValues' failed to satisfy constraint: \
108 Member must satisfy enum value set: \
109 [ALL_NEW, UPDATED_OLD, ALL_OLD, NONE, UPDATED_NEW]",
110 rv
111 ));
112 }
113 }
114
115 if let Some(ref ricm) = raw.return_item_collection_metrics {
117 if !["SIZE", "NONE"].contains(&ricm.as_str()) {
118 errors.push(format!(
119 "Value '{}' at 'returnItemCollectionMetrics' failed to satisfy constraint: \
120 Member must satisfy enum value set: [SIZE, NONE]",
121 ricm
122 ));
123 }
124 }
125
126 if let Some(msg) = format_validation_errors(&errors) {
127 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
128 }
129
130 Ok(UpdateItemRequest {
131 table_name,
132 key: raw.key.unwrap_or_default(),
133 update_expression: raw.update_expression,
134 condition_expression: raw.condition_expression,
135 expression_attribute_names: raw.expression_attribute_names,
136 expression_attribute_values: raw.expression_attribute_values,
137 return_values: raw.return_values,
138 return_consumed_capacity: raw.return_consumed_capacity,
139 return_values_on_condition_check_failure: raw.return_values_on_condition_check_failure,
140 return_item_collection_metrics: raw.return_item_collection_metrics,
141 attribute_updates: raw.attribute_updates,
142 expected: raw.expected,
143 conditional_operator: raw.conditional_operator,
144 })
145 }
146}
147
148#[derive(Debug, Clone, Default, Deserialize)]
150pub struct AttributeValueUpdate {
151 #[serde(rename = "Action", default = "default_put_action")]
152 pub action: String,
153 #[serde(rename = "Value", default)]
154 pub value: Option<AttributeValue>,
155}
156
157fn default_put_action() -> String {
158 "PUT".to_string()
159}
160
161#[derive(Debug, Default, Serialize)]
162pub struct UpdateItemResponse {
163 #[serde(rename = "Attributes", skip_serializing_if = "Option::is_none")]
164 pub attributes: Option<HashMap<String, AttributeValue>>,
165 #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
166 pub consumed_capacity: Option<types::ConsumedCapacity>,
167 #[serde(
168 rename = "ItemCollectionMetrics",
169 skip_serializing_if = "Option::is_none"
170 )]
171 pub item_collection_metrics: Option<crate::types::ItemCollectionMetrics>,
172}
173
174fn wrap_invalid_update_expression(err: String) -> String {
182 if err.starts_with("Invalid UpdateExpression:") {
183 err
184 } else {
185 format!("Invalid UpdateExpression: {err}")
186 }
187}
188
189pub async fn execute<S: StorageBackend>(
190 storage: &S,
191 mut request: UpdateItemRequest,
192) -> Result<UpdateItemResponse> {
193 crate::validation::validate_table_name(&request.table_name)?;
195
196 {
198 let mut non_expr = Vec::new();
199 let mut expr_params = Vec::new();
200 if request.attribute_updates.is_some() {
201 non_expr.push("AttributeUpdates");
202 }
203 if request.expected.is_some() {
204 non_expr.push("Expected");
205 }
206 if request.update_expression.is_some() {
207 expr_params.push("UpdateExpression");
208 }
209 if request.condition_expression.is_some() {
210 expr_params.push("ConditionExpression");
211 }
212 let no_raw_eav: Option<serde_json::Value> = None;
213 let ctx = helpers::ExpressionParamContext {
214 non_expression_params: non_expr,
215 expression_params: expr_params,
216 all_expression_param_names: vec!["UpdateExpression", "ConditionExpression"],
217 expression_attribute_names: &request.expression_attribute_names,
218 expression_attribute_values: &request.expression_attribute_values,
219 expression_attribute_values_raw: &no_raw_eav,
220 };
221 helpers::validate_expression_params(&ctx)?;
222 }
223
224 crate::validation::validate_key_attribute_values(&request.key)?;
226
227 if request.update_expression.is_none() {
229 if let Some(ref updates) = request.attribute_updates {
230 for (attr_name, update) in updates {
231 let action = update.action.to_uppercase();
232 if update.value.is_none() && action != "DELETE" {
233 return Err(DynoxideError::ValidationException(
234 "One or more parameter values were invalid: \
235 Only DELETE action is allowed when no attribute value is specified"
236 .to_string(),
237 ));
238 }
239 if action == "DELETE" {
240 if let Some(ref val) = update.value {
241 let type_name = match val {
242 AttributeValue::SS(_)
243 | AttributeValue::NS(_)
244 | AttributeValue::BS(_) => None,
245 _ => Some(val.type_name()),
246 };
247 if let Some(tn) = type_name {
248 return Err(DynoxideError::ValidationException(format!(
249 "One or more parameter values were invalid: \
250 DELETE action with value is not supported for the type {tn}"
251 )));
252 }
253 }
254 }
255 if action == "ADD" {
256 if let Some(ref val) = update.value {
257 let allowed = matches!(
258 val,
259 AttributeValue::N(_)
260 | AttributeValue::SS(_)
261 | AttributeValue::NS(_)
262 | AttributeValue::BS(_)
263 | AttributeValue::L(_)
264 );
265 if !allowed {
266 let tn = val.type_name();
267 return Err(DynoxideError::ValidationException(format!(
268 "One or more parameter values were invalid: \
269 ADD action is not supported for the type {tn}"
270 )));
271 }
272 }
273 }
274 let _ = attr_name; }
276 }
277 }
278
279 if request.condition_expression.is_none() && request.update_expression.is_none() {
281 if let Some(ref expected_val) = request.expected {
282 if let Ok(expected) = serde_json::from_value::<
283 HashMap<String, helpers::ExpectedCondition>,
284 >(expected_val.clone())
285 {
286 helpers::validate_expected_conditions(&expected)?;
287 }
288 }
289 }
290
291 if let Some(ref ue) = request.update_expression {
293 if ue.is_empty() {
294 return Err(DynoxideError::ValidationException(
295 "Invalid UpdateExpression: The expression can not be empty;".to_string(),
296 ));
297 }
298 }
299
300 if let Some(ref ce) = request.condition_expression {
302 if ce.is_empty() {
303 return Err(DynoxideError::ValidationException(
304 "Invalid ConditionExpression: The expression can not be empty;".to_string(),
305 ));
306 }
307 }
308
309 if let Some(ref ue) = request.update_expression {
313 let parsed =
314 crate::expressions::update::parse(ue).map_err(DynoxideError::ValidationException)?;
315
316 let tracker = crate::expressions::TrackedExpressionAttributes::new(
318 &request.expression_attribute_names,
319 &request.expression_attribute_values,
320 );
321 crate::expressions::update::track_references(&parsed, &tracker)
322 .map_err(|e| DynoxideError::ValidationException(wrap_invalid_update_expression(e)))?;
323
324 if let Some(ref ce) = request.condition_expression {
326 if let Ok(cond_parsed) = crate::expressions::condition::parse(ce) {
327 crate::expressions::condition::track_references(&cond_parsed, &tracker)
328 .map_err(DynoxideError::ValidationException)?;
329 }
330 }
331
332 tracker.check_unused()?;
334 }
335
336 if let Some(ref ce) = request.condition_expression {
338 let parsed = crate::expressions::condition::parse(ce).map_err(|e| {
339 DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
340 })?;
341 crate::expressions::condition::validate_static(
342 &parsed,
343 &request.expression_attribute_values,
344 )
345 .map_err(DynoxideError::ValidationException)?;
346 crate::expressions::condition::validate_operand_semantics(
347 &parsed,
348 &request.expression_attribute_names,
349 &request.expression_attribute_values,
350 )
351 .map_err(|e| {
352 DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
353 })?;
354 }
355
356 if request.condition_expression.is_none() {
358 if let Some(ref expected_val) = request.expected {
359 if let Ok(expected) = serde_json::from_value::<
360 HashMap<String, helpers::ExpectedCondition>,
361 >(expected_val.clone())
362 {
363 if !expected.is_empty() {
364 let (cond_expr, values) = helpers::convert_expected_to_condition(
365 &expected,
366 request.conditional_operator.as_deref(),
367 )?;
368 if !cond_expr.is_empty() {
369 let names = helpers::expected_attr_names(&expected);
370 request.condition_expression = Some(cond_expr);
371 let expr_values = request
372 .expression_attribute_values
373 .get_or_insert_with(HashMap::new);
374 expr_values.extend(values);
375 let expr_names = request
376 .expression_attribute_names
377 .get_or_insert_with(HashMap::new);
378 expr_names.extend(names);
379 }
380 }
381 }
382 }
383 }
384
385 let meta = helpers::require_table_for_item_op(storage, &request.table_name).await?;
386 let key_schema = helpers::parse_key_schema(&meta)?;
387
388 if let Some(ref rv) = request.return_values {
390 let rv_upper = rv.to_uppercase();
391 if !["NONE", "ALL_OLD", "ALL_NEW", "UPDATED_OLD", "UPDATED_NEW"]
392 .contains(&rv_upper.as_str())
393 {
394 return Err(DynoxideError::ValidationException(format!(
395 "1 validation error detected: Value '{rv}' at 'returnValues' failed to satisfy constraint: \
396 Member must satisfy enum value set: [ALL_NEW, ALL_OLD, NONE, UPDATED_NEW, UPDATED_OLD]"
397 )));
398 }
399 }
400
401 helpers::validate_key_only(&request.key, &key_schema)?;
403
404 let (pk, sk) = helpers::extract_key_strings(&request.key, &key_schema)?;
407
408 let legacy_attr_names: Option<Vec<String>> = request
411 .attribute_updates
412 .as_ref()
413 .map(|updates| updates.keys().cloned().collect());
414
415 let tracker = crate::expressions::TrackedExpressionAttributes::without_tracking(
419 &request.expression_attribute_names,
420 &request.expression_attribute_values,
421 );
422
423 let (
429 UpdateWorkResult {
430 old_item,
431 item,
432 item_json,
433 size,
434 },
435 gsi_units,
436 ) = helpers::with_write_transaction(storage, async {
437 let existing_json = storage.get_item(&request.table_name, &pk, &sk).await?;
439 let existing_item: HashMap<String, AttributeValue> = existing_json
440 .as_ref()
441 .and_then(|j| serde_json::from_str(j).ok())
442 .unwrap_or_default();
443
444 if let Some(ref cond_expr) = request.condition_expression {
448 let parsed = crate::expressions::condition::parse(cond_expr)
449 .map_err(DynoxideError::ValidationException)?;
450 let result = crate::expressions::condition::evaluate(&parsed, &existing_item, &tracker)
451 .map_err(DynoxideError::ValidationException)?;
452 if !result {
453 let return_item = if request.return_values_on_condition_check_failure.as_deref()
454 == Some("ALL_OLD")
455 && existing_json.is_some()
456 {
457 Some(existing_item.clone())
458 } else {
459 None
460 };
461 return Err(DynoxideError::ConditionalCheckFailedException(
462 "The conditional request failed".to_string(),
463 return_item,
464 ));
465 }
466 }
467
468 let mut item = existing_item;
471 if existing_json.is_none() {
472 for (k, v) in &request.key {
473 item.insert(k.clone(), v.clone());
474 }
475 }
476
477 let old_item = item.clone();
479
480 if let Some(ref update_expr) = request.update_expression {
482 let parsed = crate::expressions::update::parse(update_expr)
483 .map_err(DynoxideError::ValidationException)?;
484
485 for action in &parsed.set_actions {
488 validate_not_key_attr(
489 action.path.first(),
490 &key_schema,
491 &request.expression_attribute_names,
492 )?;
493 }
494
495 for path in &parsed.remove_actions {
497 validate_not_key_attr(
498 path.first(),
499 &key_schema,
500 &request.expression_attribute_names,
501 )?;
502 }
503
504 for action in &parsed.add_actions {
506 validate_not_key_attr(
507 action.path.first(),
508 &key_schema,
509 &request.expression_attribute_names,
510 )?;
511 }
512
513 for action in &parsed.delete_actions {
515 validate_not_key_attr(
516 action.path.first(),
517 &key_schema,
518 &request.expression_attribute_names,
519 )?;
520 }
521
522 crate::expressions::update::apply(&mut item, &parsed, &tracker)
523 .map_err(DynoxideError::ValidationException)?;
524 }
525
526 if request.update_expression.is_none() {
528 if let Some(ref updates) = request.attribute_updates {
529 apply_attribute_updates(&mut item, updates, &key_schema)?;
530 }
531 }
532
533 crate::validation::validate_item_attribute_values(&item)?;
539 crate::validation::normalize_item_sets(&mut item);
540
541 helpers::validate_updated_index_keys(&old_item, &item, &meta)?;
544
545 let size = types::item_size(&item);
547 if size > types::MAX_ITEM_SIZE {
548 return Err(DynoxideError::ValidationException(
549 "Item size to update has exceeded the maximum allowed size".to_string(),
550 ));
551 }
552
553 let item_json = serde_json::to_string(&item)
555 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
556 let hash_prefix = request
557 .key
558 .get(&key_schema.partition_key)
559 .map(crate::storage::compute_hash_prefix)
560 .unwrap_or_default();
561 storage
562 .put_item_with_hash(
563 &request.table_name,
564 &pk,
565 &sk,
566 &item_json,
567 size,
568 &hash_prefix,
569 )
570 .await?;
571
572 let gsi_units = super::gsi::maintain_gsis_after_write(
574 storage,
575 &request.table_name,
576 &meta,
577 &pk,
578 &sk,
579 &item,
580 &key_schema.partition_key,
581 key_schema.sort_key.as_deref(),
582 )
583 .await?;
584
585 super::lsi::maintain_lsis_after_write(
587 storage,
588 &request.table_name,
589 &meta,
590 &pk,
591 &sk,
592 &item,
593 &key_schema.partition_key,
594 key_schema.sort_key.as_deref(),
595 )
596 .await?;
597
598 let old_for_stream = if existing_json.is_some() {
600 Some(&old_item)
601 } else {
602 None
603 };
604 crate::streams::record_stream_event(storage, &meta, old_for_stream, Some(&item)).await?;
605
606 Ok((
607 UpdateWorkResult {
608 old_item,
609 item,
610 item_json,
611 size,
612 },
613 gsi_units,
614 ))
615 })
616 .await?;
617
618 let return_values = request.return_values.as_deref().unwrap_or("NONE");
620 let attributes = match return_values.to_uppercase().as_str() {
621 "ALL_OLD" => Some(old_item),
622 "ALL_NEW" => Some(item),
623 "UPDATED_OLD" => {
624 if let Some(ref update_expr) = request.update_expression {
625 let parsed = crate::expressions::update::parse(update_expr)
627 .map_err(DynoxideError::ValidationException)?;
628 omit_if_empty(extract_updated_attrs(
629 &old_item,
630 &parsed,
631 &request.expression_attribute_names,
632 ))
633 } else {
634 legacy_attr_names
636 .as_ref()
637 .map(|names| extract_named_attrs(&old_item, names))
638 .and_then(omit_if_empty)
639 }
640 }
641 "UPDATED_NEW" => {
642 if let Some(ref update_expr) = request.update_expression {
643 let parsed = crate::expressions::update::parse(update_expr)
645 .map_err(DynoxideError::ValidationException)?;
646 let new_item: HashMap<String, AttributeValue> = serde_json::from_str(&item_json)
647 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
648 omit_if_empty(extract_updated_attrs(
649 &new_item,
650 &parsed,
651 &request.expression_attribute_names,
652 ))
653 } else {
654 legacy_attr_names
656 .as_ref()
657 .map(|names| {
658 let new_item: HashMap<String, AttributeValue> =
659 serde_json::from_str(&item_json).unwrap_or_default();
660 extract_named_attrs(&new_item, names)
661 })
662 .and_then(omit_if_empty)
663 }
664 }
665 _ => None, };
667
668 let pk_value = request.key.get(&key_schema.partition_key).cloned();
670 let item_collection_metrics = helpers::build_item_collection_metrics(
671 storage,
672 &meta,
673 &request.table_name,
674 &pk,
675 &key_schema.partition_key,
676 pk_value
677 .as_ref()
678 .unwrap_or(&AttributeValue::S(String::new())),
679 &request.return_item_collection_metrics,
680 )
681 .await?;
682
683 let consumed_capacity = types::consumed_capacity_with_indexes(
684 &request.table_name,
685 types::write_capacity_units(size),
686 &gsi_units,
687 &request.return_consumed_capacity,
688 );
689
690 Ok(UpdateItemResponse {
691 attributes,
692 consumed_capacity,
693 item_collection_metrics,
694 })
695}
696
697fn apply_attribute_updates(
704 item: &mut HashMap<String, AttributeValue>,
705 updates: &HashMap<String, AttributeValueUpdate>,
706 key_schema: &helpers::KeySchema,
707) -> Result<()> {
708 for (attr_name, update) in updates {
709 if attr_name == &key_schema.partition_key
711 || key_schema
712 .sort_key
713 .as_ref()
714 .is_some_and(|sk| sk == attr_name)
715 {
716 return Err(DynoxideError::ValidationException(format!(
717 "One or more parameter values were invalid: \
718 Cannot update attribute {attr_name}. This attribute is part of the key"
719 )));
720 }
721
722 let action = update.action.to_uppercase();
723 match action.as_str() {
724 "PUT" => {
725 if let Some(ref value) = update.value {
726 item.insert(attr_name.clone(), value.clone());
727 }
728 }
729 "ADD" => {
730 if let Some(ref add_val) = update.value {
731 let path = vec![crate::expressions::PathElement::Attribute(
732 attr_name.clone(),
733 )];
734 crate::expressions::update::apply_add_public(item, &path, add_val)
735 .map_err(DynoxideError::ValidationException)?;
736 }
737 }
738 "DELETE" => {
739 if let Some(ref del_val) = update.value {
740 let path = vec![crate::expressions::PathElement::Attribute(
742 attr_name.clone(),
743 )];
744 crate::expressions::update::apply_delete_public(item, &path, del_val)
745 .map_err(DynoxideError::ValidationException)?;
746 } else {
747 item.remove(attr_name);
749 }
750 }
751 _ => {
752 return Err(DynoxideError::ValidationException(format!(
753 "1 validation error detected: Value '{action}' at 'attributeUpdates.{attr_name}.member.action' \
754 failed to satisfy constraint: Member must satisfy enum value set: [ADD, PUT, DELETE]"
755 )));
756 }
757 }
758 }
759 Ok(())
760}
761
762fn extract_updated_attrs(
771 item: &HashMap<String, AttributeValue>,
772 expr: &crate::expressions::update::UpdateExpr,
773 attr_names: &Option<HashMap<String, String>>,
774) -> HashMap<String, AttributeValue> {
775 use crate::expressions::{PathElement, resolve_path, resolve_path_elements};
776
777 let no_values: Option<HashMap<String, AttributeValue>> = None;
778 let tracker =
779 crate::expressions::TrackedExpressionAttributes::without_tracking(attr_names, &no_values);
780
781 let mut paths: Vec<&[PathElement]> = Vec::new();
783 paths.extend(expr.set_actions.iter().map(|a| a.path.as_slice()));
784 paths.extend(expr.remove_actions.iter().map(|p| p.as_slice()));
785 paths.extend(expr.add_actions.iter().map(|a| a.path.as_slice()));
786 paths.extend(expr.delete_actions.iter().map(|a| a.path.as_slice()));
787
788 let mut result = HashMap::new();
789 for path in paths {
790 let Ok(resolved) = resolve_path_elements(path, &tracker) else {
791 continue;
792 };
793
794 if resolved.iter().any(|e| matches!(e, PathElement::Index(_))) {
801 if let Some(PathElement::Attribute(top)) = resolved.first() {
802 if let Some(val) = item.get(top) {
803 result.insert(top.clone(), val.clone());
804 }
805 }
806 continue;
807 }
808
809 if let Some(val) = resolve_path(item, &resolved) {
810 crate::expressions::projection::insert_at_path(&mut result, &resolved, val);
811 }
812 }
813
814 result
815}
816
817fn omit_if_empty(map: HashMap<String, AttributeValue>) -> Option<HashMap<String, AttributeValue>> {
824 if map.is_empty() { None } else { Some(map) }
825}
826
827fn extract_named_attrs(
829 item: &HashMap<String, AttributeValue>,
830 attr_names: &[String],
831) -> HashMap<String, AttributeValue> {
832 let mut result = HashMap::new();
833 for name in attr_names {
834 if let Some(val) = item.get(name) {
835 result.insert(name.clone(), val.clone());
836 }
837 }
838 result
839}
840
841fn validate_not_key_attr(
843 first_element: Option<&crate::expressions::PathElement>,
844 key_schema: &helpers::KeySchema,
845 expression_attribute_names: &Option<HashMap<String, String>>,
846) -> crate::errors::Result<()> {
847 if let Some(crate::expressions::PathElement::Attribute(name)) = first_element {
848 let resolved_name = if name.starts_with('#') {
849 crate::expressions::resolve_name(name, expression_attribute_names)
850 .map_err(DynoxideError::ValidationException)?
851 } else {
852 name.clone()
853 };
854 if resolved_name == key_schema.partition_key
855 || key_schema
856 .sort_key
857 .as_ref()
858 .is_some_and(|sk| sk == &resolved_name)
859 {
860 return Err(DynoxideError::ValidationException(format!(
861 "One or more parameter values were invalid: Cannot update attribute {resolved_name}. This attribute is part of the key"
862 )));
863 }
864 }
865 Ok(())
866}
867
868#[cfg(test)]
869mod tests {
870 use crate::actions::{create_table, put_item, update_item};
871 use crate::storage::Storage;
872 use crate::storage_backend::StorageBackend;
873
874 #[test]
877 fn update_item_rolls_back_base_write_when_gsi_fan_out_fails() {
878 let storage = Storage::memory().unwrap();
879
880 let create = serde_json::from_value(serde_json::json!({
881 "TableName": "Orders",
882 "KeySchema": [{"AttributeName": "UserId", "KeyType": "HASH"}],
883 "AttributeDefinitions": [
884 {"AttributeName": "UserId", "AttributeType": "S"},
885 {"AttributeName": "Status", "AttributeType": "S"},
886 {"AttributeName": "Priority", "AttributeType": "S"}
887 ],
888 "GlobalSecondaryIndexes": [
889 {"IndexName": "StatusIndex", "KeySchema": [{"AttributeName": "Status", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}},
890 {"IndexName": "PriorityIndex", "KeySchema": [{"AttributeName": "Priority", "KeyType": "HASH"}], "Projection": {"ProjectionType": "ALL"}}
891 ]
892 }))
893 .unwrap();
894 pollster::block_on(create_table::execute(&storage, create)).unwrap();
895
896 let put = serde_json::from_value(serde_json::json!({
897 "TableName": "Orders",
898 "Item": {"UserId": {"S": "u1"}, "Status": {"S": "SHIPPED"}, "Priority": {"S": "HIGH"}, "Note": {"S": "before"}}
899 }))
900 .unwrap();
901 pollster::block_on(put_item::execute(&storage, put)).unwrap();
902
903 storage.drop_gsi_table("Orders", "PriorityIndex").unwrap();
905
906 let update = serde_json::from_value(serde_json::json!({
907 "TableName": "Orders",
908 "Key": {"UserId": {"S": "u1"}},
909 "UpdateExpression": "SET Note = :n",
910 "ExpressionAttributeValues": {":n": {"S": "after"}}
911 }))
912 .unwrap();
913 let res = pollster::block_on(update_item::execute(&storage, update));
914 assert!(
915 res.is_err(),
916 "a mid-fan-out failure must surface as an error"
917 );
918
919 let rows = pollster::block_on(<Storage as StorageBackend>::scan_items(
922 &storage,
923 "Orders",
924 &Default::default(),
925 ))
926 .unwrap();
927 assert_eq!(
928 rows.len(),
929 1,
930 "the item must still be present after rollback"
931 );
932 let raw = &rows[0].2;
933 assert!(
934 raw.contains("\"before\"") && !raw.contains("\"after\""),
935 "update must roll back when fan-out fails, leaving the original value: {raw}"
936 );
937 }
938}