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::{
71 TableNameContext, format_validation_errors, table_name_constraint_errors,
72 };
73
74 let mut errors = Vec::new();
75
76 errors.extend(table_name_constraint_errors(
78 raw.table_name.as_deref(),
79 TableNameContext::ReadWrite,
80 ));
81 let table_name = raw.table_name.unwrap_or_default();
82
83 if raw.key.is_none() {
85 errors.push(
86 "Value null at 'key' failed to satisfy constraint: \
87 Member must not be null"
88 .to_string(),
89 );
90 }
91
92 if let Some(ref rcc) = raw.return_consumed_capacity {
94 if !["INDEXES", "TOTAL", "NONE"].contains(&rcc.as_str()) {
95 errors.push(format!(
96 "Value '{}' at 'returnConsumedCapacity' failed to satisfy constraint: \
97 Member must satisfy enum value set: [INDEXES, TOTAL, NONE]",
98 rcc
99 ));
100 }
101 }
102
103 if let Some(ref rv) = raw.return_values {
105 if !["ALL_NEW", "UPDATED_OLD", "ALL_OLD", "NONE", "UPDATED_NEW"].contains(&rv.as_str())
106 {
107 errors.push(format!(
108 "Value '{}' at 'returnValues' failed to satisfy constraint: \
109 Member must satisfy enum value set: \
110 [ALL_NEW, UPDATED_OLD, ALL_OLD, NONE, UPDATED_NEW]",
111 rv
112 ));
113 }
114 }
115
116 if let Some(ref ricm) = raw.return_item_collection_metrics {
118 if !["SIZE", "NONE"].contains(&ricm.as_str()) {
119 errors.push(format!(
120 "Value '{}' at 'returnItemCollectionMetrics' failed to satisfy constraint: \
121 Member must satisfy enum value set: [SIZE, NONE]",
122 ricm
123 ));
124 }
125 }
126
127 if let Some(msg) = format_validation_errors(&errors) {
128 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
129 }
130
131 Ok(UpdateItemRequest {
132 table_name,
133 key: raw.key.unwrap_or_default(),
134 update_expression: raw.update_expression,
135 condition_expression: raw.condition_expression,
136 expression_attribute_names: raw.expression_attribute_names,
137 expression_attribute_values: raw.expression_attribute_values,
138 return_values: raw.return_values,
139 return_consumed_capacity: raw.return_consumed_capacity,
140 return_values_on_condition_check_failure: raw.return_values_on_condition_check_failure,
141 return_item_collection_metrics: raw.return_item_collection_metrics,
142 attribute_updates: raw.attribute_updates,
143 expected: raw.expected,
144 conditional_operator: raw.conditional_operator,
145 })
146 }
147}
148
149#[derive(Debug, Clone, Default, Deserialize)]
151pub struct AttributeValueUpdate {
152 #[serde(rename = "Action", default = "default_put_action")]
153 pub action: String,
154 #[serde(rename = "Value", default)]
155 pub value: Option<AttributeValue>,
156}
157
158fn default_put_action() -> String {
159 "PUT".to_string()
160}
161
162#[derive(Debug, Default, Serialize)]
163pub struct UpdateItemResponse {
164 #[serde(rename = "Attributes", skip_serializing_if = "Option::is_none")]
165 pub attributes: Option<HashMap<String, AttributeValue>>,
166 #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
167 pub consumed_capacity: Option<types::ConsumedCapacity>,
168 #[serde(
169 rename = "ItemCollectionMetrics",
170 skip_serializing_if = "Option::is_none"
171 )]
172 pub item_collection_metrics: Option<crate::types::ItemCollectionMetrics>,
173}
174
175fn wrap_invalid_update_expression(err: String) -> String {
183 if err.starts_with("Invalid UpdateExpression:") {
184 err
185 } else {
186 format!("Invalid UpdateExpression: {err}")
187 }
188}
189
190pub fn execute(storage: &Storage, mut request: UpdateItemRequest) -> Result<UpdateItemResponse> {
191 crate::validation::validate_table_name(&request.table_name)?;
193
194 {
196 let mut non_expr = Vec::new();
197 let mut expr_params = Vec::new();
198 if request.attribute_updates.is_some() {
199 non_expr.push("AttributeUpdates");
200 }
201 if request.expected.is_some() {
202 non_expr.push("Expected");
203 }
204 if request.update_expression.is_some() {
205 expr_params.push("UpdateExpression");
206 }
207 if request.condition_expression.is_some() {
208 expr_params.push("ConditionExpression");
209 }
210 let no_raw_eav: Option<serde_json::Value> = None;
211 let ctx = helpers::ExpressionParamContext {
212 non_expression_params: non_expr,
213 expression_params: expr_params,
214 all_expression_param_names: vec!["UpdateExpression", "ConditionExpression"],
215 expression_attribute_names: &request.expression_attribute_names,
216 expression_attribute_values: &request.expression_attribute_values,
217 expression_attribute_values_raw: &no_raw_eav,
218 };
219 helpers::validate_expression_params(&ctx)?;
220 }
221
222 crate::validation::validate_key_attribute_values(&request.key)?;
224
225 if request.update_expression.is_none() {
227 if let Some(ref updates) = request.attribute_updates {
228 for (attr_name, update) in updates {
229 let action = update.action.to_uppercase();
230 if update.value.is_none() && action != "DELETE" {
231 return Err(DynoxideError::ValidationException(
232 "One or more parameter values were invalid: \
233 Only DELETE action is allowed when no attribute value is specified"
234 .to_string(),
235 ));
236 }
237 if action == "DELETE" {
238 if let Some(ref val) = update.value {
239 let type_name = match val {
240 AttributeValue::SS(_)
241 | AttributeValue::NS(_)
242 | AttributeValue::BS(_) => None,
243 _ => Some(val.type_name()),
244 };
245 if let Some(tn) = type_name {
246 return Err(DynoxideError::ValidationException(format!(
247 "One or more parameter values were invalid: \
248 DELETE action with value is not supported for the type {tn}"
249 )));
250 }
251 }
252 }
253 if action == "ADD" {
254 if let Some(ref val) = update.value {
255 let allowed = matches!(
256 val,
257 AttributeValue::N(_)
258 | AttributeValue::SS(_)
259 | AttributeValue::NS(_)
260 | AttributeValue::BS(_)
261 | AttributeValue::L(_)
262 );
263 if !allowed {
264 let tn = val.type_name();
265 return Err(DynoxideError::ValidationException(format!(
266 "One or more parameter values were invalid: \
267 ADD action is not supported for the type {tn}"
268 )));
269 }
270 }
271 }
272 let _ = attr_name; }
274 }
275 }
276
277 if request.condition_expression.is_none() && request.update_expression.is_none() {
279 if let Some(ref expected_val) = request.expected {
280 if let Ok(expected) = serde_json::from_value::<
281 HashMap<String, helpers::ExpectedCondition>,
282 >(expected_val.clone())
283 {
284 helpers::validate_expected_conditions(&expected)?;
285 }
286 }
287 }
288
289 if let Some(ref ue) = request.update_expression {
291 if ue.is_empty() {
292 return Err(DynoxideError::ValidationException(
293 "Invalid UpdateExpression: The expression can not be empty;".to_string(),
294 ));
295 }
296 }
297
298 if let Some(ref ce) = request.condition_expression {
300 if ce.is_empty() {
301 return Err(DynoxideError::ValidationException(
302 "Invalid ConditionExpression: The expression can not be empty;".to_string(),
303 ));
304 }
305 }
306
307 if let Some(ref ue) = request.update_expression {
311 let parsed =
312 crate::expressions::update::parse(ue).map_err(DynoxideError::ValidationException)?;
313
314 let tracker = crate::expressions::TrackedExpressionAttributes::new(
316 &request.expression_attribute_names,
317 &request.expression_attribute_values,
318 );
319 crate::expressions::update::track_references(&parsed, &tracker)
320 .map_err(|e| DynoxideError::ValidationException(wrap_invalid_update_expression(e)))?;
321
322 if let Some(ref ce) = request.condition_expression {
324 if let Ok(cond_parsed) = crate::expressions::condition::parse(ce) {
325 crate::expressions::condition::track_references(&cond_parsed, &tracker)
326 .map_err(DynoxideError::ValidationException)?;
327 }
328 }
329
330 tracker.check_unused()?;
332 }
333
334 if let Some(ref ce) = request.condition_expression {
336 let parsed = crate::expressions::condition::parse(ce).map_err(|e| {
337 DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
338 })?;
339 crate::expressions::condition::validate_static(
340 &parsed,
341 &request.expression_attribute_values,
342 )
343 .map_err(DynoxideError::ValidationException)?;
344 }
345
346 if request.condition_expression.is_none() {
348 if let Some(ref expected_val) = request.expected {
349 if let Ok(expected) = serde_json::from_value::<
350 HashMap<String, helpers::ExpectedCondition>,
351 >(expected_val.clone())
352 {
353 if !expected.is_empty() {
354 let (cond_expr, values) = helpers::convert_expected_to_condition(
355 &expected,
356 request.conditional_operator.as_deref(),
357 )?;
358 if !cond_expr.is_empty() {
359 let names = helpers::expected_attr_names(&expected);
360 request.condition_expression = Some(cond_expr);
361 let expr_values = request
362 .expression_attribute_values
363 .get_or_insert_with(HashMap::new);
364 expr_values.extend(values);
365 let expr_names = request
366 .expression_attribute_names
367 .get_or_insert_with(HashMap::new);
368 expr_names.extend(names);
369 }
370 }
371 }
372 }
373 }
374
375 let meta = helpers::require_table_for_item_op(storage, &request.table_name)?;
376 let key_schema = helpers::parse_key_schema(&meta)?;
377
378 if let Some(ref rv) = request.return_values {
380 let rv_upper = rv.to_uppercase();
381 if !["NONE", "ALL_OLD", "ALL_NEW", "UPDATED_OLD", "UPDATED_NEW"]
382 .contains(&rv_upper.as_str())
383 {
384 return Err(DynoxideError::ValidationException(format!(
385 "1 validation error detected: Value '{rv}' at 'returnValues' failed to satisfy constraint: \
386 Member must satisfy enum value set: [ALL_NEW, ALL_OLD, NONE, UPDATED_NEW, UPDATED_OLD]"
387 )));
388 }
389 }
390
391 helpers::validate_key_only(&request.key, &key_schema)?;
393
394 let (pk, sk) = helpers::extract_key_strings(&request.key, &key_schema)?;
397
398 let legacy_attr_names: Option<Vec<String>> = request
401 .attribute_updates
402 .as_ref()
403 .map(|updates| updates.keys().cloned().collect());
404
405 let has_condition = request.condition_expression.is_some();
407 if has_condition {
408 storage.begin_transaction()?;
409 }
410
411 let tracker = crate::expressions::TrackedExpressionAttributes::without_tracking(
415 &request.expression_attribute_names,
416 &request.expression_attribute_values,
417 );
418
419 let transactional_work = || -> Result<UpdateWorkResult> {
422 let existing_json = storage.get_item(&request.table_name, &pk, &sk)?;
424 let existing_item: HashMap<String, AttributeValue> = existing_json
425 .as_ref()
426 .and_then(|j| serde_json::from_str(j).ok())
427 .unwrap_or_default();
428
429 if let Some(ref cond_expr) = request.condition_expression {
433 let parsed = crate::expressions::condition::parse(cond_expr)
434 .map_err(DynoxideError::ValidationException)?;
435 let result = crate::expressions::condition::evaluate(&parsed, &existing_item, &tracker)
436 .map_err(DynoxideError::ValidationException)?;
437 if !result {
438 let return_item = if request.return_values_on_condition_check_failure.as_deref()
439 == Some("ALL_OLD")
440 && existing_json.is_some()
441 {
442 Some(existing_item.clone())
443 } else {
444 None
445 };
446 return Err(DynoxideError::ConditionalCheckFailedException(
447 "The conditional request failed".to_string(),
448 return_item,
449 ));
450 }
451 }
452
453 let mut item = existing_item;
456 if existing_json.is_none() {
457 for (k, v) in &request.key {
458 item.insert(k.clone(), v.clone());
459 }
460 }
461
462 let old_item = item.clone();
464
465 if let Some(ref update_expr) = request.update_expression {
467 let parsed = crate::expressions::update::parse(update_expr)
468 .map_err(DynoxideError::ValidationException)?;
469
470 for action in &parsed.set_actions {
473 validate_not_key_attr(
474 action.path.first(),
475 &key_schema,
476 &request.expression_attribute_names,
477 )?;
478 }
479
480 for path in &parsed.remove_actions {
482 validate_not_key_attr(
483 path.first(),
484 &key_schema,
485 &request.expression_attribute_names,
486 )?;
487 }
488
489 for action in &parsed.add_actions {
491 validate_not_key_attr(
492 action.path.first(),
493 &key_schema,
494 &request.expression_attribute_names,
495 )?;
496 }
497
498 for action in &parsed.delete_actions {
500 validate_not_key_attr(
501 action.path.first(),
502 &key_schema,
503 &request.expression_attribute_names,
504 )?;
505 }
506
507 crate::expressions::update::apply(&mut item, &parsed, &tracker)
508 .map_err(DynoxideError::ValidationException)?;
509 }
510
511 if request.update_expression.is_none() {
513 if let Some(ref updates) = request.attribute_updates {
514 apply_attribute_updates(&mut item, updates, &key_schema)?;
515 }
516 }
517
518 crate::validation::validate_item_attribute_values(&item)?;
524 crate::validation::normalize_item_sets(&mut item);
525
526 let size = types::item_size(&item);
528 if size > types::MAX_ITEM_SIZE {
529 return Err(DynoxideError::ValidationException(
530 "Item size to update has exceeded the maximum allowed size".to_string(),
531 ));
532 }
533
534 let item_json = serde_json::to_string(&item)
536 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
537 let hash_prefix = request
538 .key
539 .get(&key_schema.partition_key)
540 .map(crate::storage::compute_hash_prefix)
541 .unwrap_or_default();
542 storage.put_item_with_hash(
543 &request.table_name,
544 &pk,
545 &sk,
546 &item_json,
547 size,
548 &hash_prefix,
549 )?;
550
551 Ok(UpdateWorkResult {
552 existing_json,
553 old_item,
554 item,
555 item_json,
556 size,
557 })
558 };
559
560 let result = transactional_work();
561
562 if has_condition {
564 match result {
565 Ok(_) => storage.commit()?,
566 Err(ref _e) => {
567 let _ = storage.rollback();
568 }
569 }
570 }
571
572 let UpdateWorkResult {
573 existing_json,
574 old_item,
575 item,
576 item_json,
577 size,
578 } = result?;
579
580 let gsi_units = super::gsi::maintain_gsis_after_write(
582 storage,
583 &request.table_name,
584 &meta,
585 &pk,
586 &sk,
587 &item,
588 &key_schema.partition_key,
589 key_schema.sort_key.as_deref(),
590 )?;
591
592 super::lsi::maintain_lsis_after_write(
594 storage,
595 &request.table_name,
596 &meta,
597 &pk,
598 &sk,
599 &item,
600 &key_schema.partition_key,
601 key_schema.sort_key.as_deref(),
602 )?;
603
604 let old_for_stream = if existing_json.is_some() {
606 Some(&old_item)
607 } else {
608 None
609 };
610 crate::streams::record_stream_event(storage, &meta, old_for_stream, Some(&item))?;
611
612 let return_values = request.return_values.as_deref().unwrap_or("NONE");
614 let attributes = match return_values.to_uppercase().as_str() {
615 "ALL_OLD" => Some(old_item),
616 "ALL_NEW" => Some(item),
617 "UPDATED_OLD" => {
618 if let Some(ref update_expr) = request.update_expression {
619 let parsed = crate::expressions::update::parse(update_expr)
621 .map_err(DynoxideError::ValidationException)?;
622 Some(extract_updated_attrs(
623 &old_item,
624 &parsed,
625 &request.expression_attribute_names,
626 ))
627 } else {
628 legacy_attr_names
630 .as_ref()
631 .map(|names| extract_named_attrs(&old_item, names))
632 }
633 }
634 "UPDATED_NEW" => {
635 if let Some(ref update_expr) = request.update_expression {
636 let parsed = crate::expressions::update::parse(update_expr)
638 .map_err(DynoxideError::ValidationException)?;
639 let new_item: HashMap<String, AttributeValue> = serde_json::from_str(&item_json)
640 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
641 Some(extract_updated_attrs(
642 &new_item,
643 &parsed,
644 &request.expression_attribute_names,
645 ))
646 } else {
647 legacy_attr_names.as_ref().map(|names| {
649 let new_item: HashMap<String, AttributeValue> =
650 serde_json::from_str(&item_json).unwrap_or_default();
651 extract_named_attrs(&new_item, names)
652 })
653 }
654 }
655 _ => None, };
657
658 let pk_value = request.key.get(&key_schema.partition_key).cloned();
660 let item_collection_metrics = helpers::build_item_collection_metrics(
661 storage,
662 &meta,
663 &request.table_name,
664 &pk,
665 &key_schema.partition_key,
666 pk_value
667 .as_ref()
668 .unwrap_or(&AttributeValue::S(String::new())),
669 &request.return_item_collection_metrics,
670 )?;
671
672 let consumed_capacity = types::consumed_capacity_with_indexes(
673 &request.table_name,
674 types::write_capacity_units(size),
675 &gsi_units,
676 &request.return_consumed_capacity,
677 );
678
679 Ok(UpdateItemResponse {
680 attributes,
681 consumed_capacity,
682 item_collection_metrics,
683 })
684}
685
686fn apply_attribute_updates(
693 item: &mut HashMap<String, AttributeValue>,
694 updates: &HashMap<String, AttributeValueUpdate>,
695 key_schema: &helpers::KeySchema,
696) -> Result<()> {
697 for (attr_name, update) in updates {
698 if attr_name == &key_schema.partition_key
700 || key_schema
701 .sort_key
702 .as_ref()
703 .is_some_and(|sk| sk == attr_name)
704 {
705 return Err(DynoxideError::ValidationException(format!(
706 "One or more parameter values were invalid: \
707 Cannot update attribute {attr_name}. This attribute is part of the key"
708 )));
709 }
710
711 let action = update.action.to_uppercase();
712 match action.as_str() {
713 "PUT" => {
714 if let Some(ref value) = update.value {
715 item.insert(attr_name.clone(), value.clone());
716 }
717 }
718 "ADD" => {
719 if let Some(ref add_val) = update.value {
720 let path = vec![crate::expressions::PathElement::Attribute(
721 attr_name.clone(),
722 )];
723 crate::expressions::update::apply_add_public(item, &path, add_val)
724 .map_err(DynoxideError::ValidationException)?;
725 }
726 }
727 "DELETE" => {
728 if let Some(ref del_val) = update.value {
729 let path = vec![crate::expressions::PathElement::Attribute(
731 attr_name.clone(),
732 )];
733 crate::expressions::update::apply_delete_public(item, &path, del_val)
734 .map_err(DynoxideError::ValidationException)?;
735 } else {
736 item.remove(attr_name);
738 }
739 }
740 _ => {
741 return Err(DynoxideError::ValidationException(format!(
742 "1 validation error detected: Value '{action}' at 'attributeUpdates.{attr_name}.member.action' \
743 failed to satisfy constraint: Member must satisfy enum value set: [ADD, PUT, DELETE]"
744 )));
745 }
746 }
747 }
748 Ok(())
749}
750
751fn extract_updated_attrs(
753 item: &HashMap<String, AttributeValue>,
754 expr: &crate::expressions::update::UpdateExpr,
755 attr_names: &Option<HashMap<String, String>>,
756) -> HashMap<String, AttributeValue> {
757 let mut result = HashMap::new();
758
759 for action in &expr.set_actions {
761 if let Some(name) = get_top_level_name(&action.path, attr_names) {
762 if let Some(val) = item.get(&name) {
763 result.insert(name, val.clone());
764 }
765 }
766 }
767
768 for path in &expr.remove_actions {
770 if let Some(name) = get_top_level_name(path, attr_names) {
771 if let Some(val) = item.get(&name) {
772 result.insert(name, val.clone());
773 }
774 }
775 }
776
777 for action in &expr.add_actions {
779 if let Some(name) = get_top_level_name(&action.path, attr_names) {
780 if let Some(val) = item.get(&name) {
781 result.insert(name, val.clone());
782 }
783 }
784 }
785
786 for action in &expr.delete_actions {
788 if let Some(name) = get_top_level_name(&action.path, attr_names) {
789 if let Some(val) = item.get(&name) {
790 result.insert(name, val.clone());
791 }
792 }
793 }
794
795 result
796}
797
798fn extract_named_attrs(
800 item: &HashMap<String, AttributeValue>,
801 attr_names: &[String],
802) -> HashMap<String, AttributeValue> {
803 let mut result = HashMap::new();
804 for name in attr_names {
805 if let Some(val) = item.get(name) {
806 result.insert(name.clone(), val.clone());
807 }
808 }
809 result
810}
811
812fn get_top_level_name(
813 path: &[crate::expressions::PathElement],
814 attr_names: &Option<HashMap<String, String>>,
815) -> Option<String> {
816 match path.first() {
817 Some(crate::expressions::PathElement::Attribute(name)) => {
818 if name.starts_with('#') {
819 crate::expressions::resolve_name(name, attr_names).ok()
820 } else {
821 Some(name.clone())
822 }
823 }
824 _ => None,
825 }
826}
827
828fn validate_not_key_attr(
830 first_element: Option<&crate::expressions::PathElement>,
831 key_schema: &helpers::KeySchema,
832 expression_attribute_names: &Option<HashMap<String, String>>,
833) -> crate::errors::Result<()> {
834 if let Some(crate::expressions::PathElement::Attribute(name)) = first_element {
835 let resolved_name = if name.starts_with('#') {
836 crate::expressions::resolve_name(name, expression_attribute_names)
837 .map_err(DynoxideError::ValidationException)?
838 } else {
839 name.clone()
840 };
841 if resolved_name == key_schema.partition_key
842 || key_schema
843 .sort_key
844 .as_ref()
845 .is_some_and(|sk| sk == &resolved_name)
846 {
847 return Err(DynoxideError::ValidationException(format!(
848 "One or more parameter values were invalid: Cannot update attribute {resolved_name}. This attribute is part of the key"
849 )));
850 }
851 }
852 Ok(())
853}