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