1use crate::actions::helpers;
2use crate::errors::{CancellationReason, DynoxideError, Result};
3use crate::storage_backend::StorageBackend;
4use crate::types::{self, AttributeValue, Item};
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet};
7
8#[derive(Debug, Clone, Default, Deserialize, Serialize)]
9pub struct TransactWriteItemsRequest {
10 #[serde(rename = "TransactItems")]
11 pub transact_items: Vec<TransactWriteItem>,
12 #[serde(rename = "ClientRequestToken", default)]
13 pub client_request_token: Option<String>,
14 #[serde(rename = "ReturnConsumedCapacity", default)]
15 pub return_consumed_capacity: Option<String>,
16 #[serde(rename = "ReturnItemCollectionMetrics", default)]
17 pub return_item_collection_metrics: Option<String>,
18}
19
20#[derive(Debug, Clone, Default, Deserialize, Serialize)]
21pub struct TransactWriteItem {
22 #[serde(rename = "Put", default)]
23 pub put: Option<TransactPut>,
24 #[serde(rename = "Update", default)]
25 pub update: Option<TransactUpdate>,
26 #[serde(rename = "Delete", default)]
27 pub delete: Option<TransactDelete>,
28 #[serde(rename = "ConditionCheck", default)]
29 pub condition_check: Option<TransactConditionCheck>,
30}
31
32#[derive(Debug, Clone, Default, Deserialize, Serialize)]
33pub struct TransactPut {
34 #[serde(rename = "TableName")]
35 pub table_name: String,
36 #[serde(rename = "Item")]
37 pub item: Item,
38 #[serde(rename = "ConditionExpression", default)]
39 pub condition_expression: Option<String>,
40 #[serde(rename = "ExpressionAttributeNames", default)]
41 pub expression_attribute_names: Option<HashMap<String, String>>,
42 #[serde(rename = "ExpressionAttributeValues", default)]
43 pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
44 #[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
45 pub return_values_on_condition_check_failure: Option<String>,
46}
47
48#[derive(Debug, Clone, Default, Deserialize, Serialize)]
49pub struct TransactUpdate {
50 #[serde(rename = "TableName")]
51 pub table_name: String,
52 #[serde(rename = "Key")]
53 pub key: HashMap<String, AttributeValue>,
54 #[serde(rename = "UpdateExpression")]
55 pub update_expression: String,
56 #[serde(rename = "ConditionExpression", default)]
57 pub condition_expression: Option<String>,
58 #[serde(rename = "ExpressionAttributeNames", default)]
59 pub expression_attribute_names: Option<HashMap<String, String>>,
60 #[serde(rename = "ExpressionAttributeValues", default)]
61 pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
62 #[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
63 pub return_values_on_condition_check_failure: Option<String>,
64}
65
66#[derive(Debug, Clone, Default, Deserialize, Serialize)]
67pub struct TransactDelete {
68 #[serde(rename = "TableName")]
69 pub table_name: String,
70 #[serde(rename = "Key")]
71 pub key: HashMap<String, AttributeValue>,
72 #[serde(rename = "ConditionExpression", default)]
73 pub condition_expression: Option<String>,
74 #[serde(rename = "ExpressionAttributeNames", default)]
75 pub expression_attribute_names: Option<HashMap<String, String>>,
76 #[serde(rename = "ExpressionAttributeValues", default)]
77 pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
78 #[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
79 pub return_values_on_condition_check_failure: Option<String>,
80}
81
82#[derive(Debug, Clone, Default, Deserialize, Serialize)]
83pub struct TransactConditionCheck {
84 #[serde(rename = "TableName")]
85 pub table_name: String,
86 #[serde(rename = "Key")]
87 pub key: HashMap<String, AttributeValue>,
88 #[serde(rename = "ConditionExpression")]
89 pub condition_expression: String,
90 #[serde(rename = "ExpressionAttributeNames", default)]
91 pub expression_attribute_names: Option<HashMap<String, String>>,
92 #[serde(rename = "ExpressionAttributeValues", default)]
93 pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
94 #[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
95 pub return_values_on_condition_check_failure: Option<String>,
96}
97
98#[derive(Debug, Clone, Default, Serialize)]
99pub struct TransactWriteItemsResponse {
100 #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
101 pub consumed_capacity: Option<Vec<crate::types::ConsumedCapacity>>,
102 #[serde(
105 rename = "ItemCollectionMetrics",
106 skip_serializing_if = "Option::is_none"
107 )]
108 pub item_collection_metrics: Option<HashMap<String, Vec<crate::types::ItemCollectionMetrics>>>,
109}
110
111pub async fn execute<S: StorageBackend>(
112 storage: &S,
113 request: TransactWriteItemsRequest,
114) -> Result<TransactWriteItemsResponse> {
115 let items = &request.transact_items;
116
117 if items.is_empty() {
119 return Err(DynoxideError::ValidationException(
120 "1 validation error detected: Value '[]' at 'transactItems' failed to satisfy constraint: Member must have length greater than or equal to 1".to_string(),
121 ));
122 }
123
124 if items.len() > 100 {
130 let dump = format!("{items:?}");
131 return Err(DynoxideError::ValidationException(format!(
132 "1 validation error detected: Value '[{dump}]' at 'transactItems' failed to satisfy constraint: Member must have length less than or equal to 100"
133 )));
134 }
135
136 let mut seen_targets = HashSet::new();
139 for item in items {
140 if let Some(target) = get_item_target(storage, item).await? {
141 if !seen_targets.insert(target) {
142 return Err(DynoxideError::ValidationException(
143 "Transaction request cannot include multiple operations on one item"
144 .to_string(),
145 ));
146 }
147 }
148 }
149
150 let total_size: usize = items.iter().map(|i| get_action_table_and_size(i).1).sum();
152 if total_size > 4 * 1024 * 1024 {
153 return Err(DynoxideError::ValidationException(
154 "Collection size of items exceeded, which can also be caused by the aggregate size of the items in the transaction exceeding the 4MB limit".to_string(),
155 ));
156 }
157
158 helpers::with_write_transaction(storage, execute_within_transaction(storage, items)).await?;
160
161 let consumed_capacity = crate::types::build_transactional_capacity(
163 &transact_write_table_units(items),
164 &request.return_consumed_capacity,
165 crate::types::transactional_write_capacity,
166 );
167 Ok(TransactWriteItemsResponse {
168 consumed_capacity,
169 item_collection_metrics: None,
170 })
171}
172
173pub(crate) fn transact_write_table_units(items: &[TransactWriteItem]) -> HashMap<String, f64> {
179 let mut table_units: HashMap<String, f64> = HashMap::new();
180 for item in items {
181 let (table, size) = get_action_table_and_size(item);
182 *table_units.entry(table).or_default() +=
183 crate::types::TRANSACTIONAL_CAPACITY_FACTOR * crate::types::write_capacity_units(size);
184 }
185 table_units
186}
187
188fn transact_read_table_units(items: &[TransactWriteItem]) -> HashMap<String, f64> {
194 let mut table_units: HashMap<String, f64> = HashMap::new();
195 for item in items {
196 let (table, size) = get_action_table_and_size(item);
197 *table_units.entry(table).or_default() +=
198 crate::types::TRANSACTIONAL_CAPACITY_FACTOR * crate::types::read_capacity_units(size);
199 }
200 table_units
201}
202
203pub(crate) fn replay_response(
212 items: &[TransactWriteItem],
213 mode: &Option<String>,
214 cached_metrics: Option<HashMap<String, Vec<crate::types::ItemCollectionMetrics>>>,
215) -> TransactWriteItemsResponse {
216 TransactWriteItemsResponse {
217 consumed_capacity: crate::types::build_transactional_capacity(
218 &transact_read_table_units(items),
219 mode,
220 crate::types::transactional_read_capacity,
221 ),
222 item_collection_metrics: cached_metrics,
223 }
224}
225
226async fn execute_within_transaction<S: StorageBackend>(
227 storage: &S,
228 items: &[TransactWriteItem],
229) -> Result<()> {
230 let mut cancellation_reasons: Vec<CancellationReason> = Vec::with_capacity(items.len());
231 let mut has_failure = false;
232
233 for item in items {
234 let reason = execute_single_action(storage, item).await;
235 match reason {
236 Ok(()) => {
237 cancellation_reasons.push(CancellationReason {
238 code: "None".to_string(),
239 message: None,
240 item: None,
241 });
242 }
243 Err(e) => {
244 if matches!(e, DynoxideError::KeyEmptyValueValidation(_)) {
248 return Err(e);
249 }
250 has_failure = true;
251 let message = Some(e.to_string());
252 let (code, item) = match e {
253 DynoxideError::ConditionalCheckFailedException(_, item) => {
254 ("ConditionalCheckFailed".to_string(), item)
255 }
256 DynoxideError::ValidationException(_) => ("ValidationError".to_string(), None),
257 _ => ("InternalError".to_string(), None),
258 };
259 cancellation_reasons.push(CancellationReason {
260 code,
261 message,
262 item,
263 });
264 }
265 }
266 }
267
268 if has_failure {
269 let codes: Vec<&str> = cancellation_reasons
270 .iter()
271 .map(|r| r.code.as_str())
272 .collect();
273 let message = format!(
274 "Transaction cancelled, please refer cancellation reasons for specific reasons [{}]",
275 codes.join(", ")
276 );
277 return Err(DynoxideError::TransactionCanceledException(
278 message,
279 cancellation_reasons,
280 ));
281 }
282
283 Ok(())
284}
285
286async fn execute_single_action<S: StorageBackend>(
287 storage: &S,
288 item: &TransactWriteItem,
289) -> Result<()> {
290 if let Some(ref put) = item.put {
291 execute_put(storage, put).await
292 } else if let Some(ref update) = item.update {
293 execute_update(storage, update).await
294 } else if let Some(ref delete) = item.delete {
295 execute_delete(storage, delete).await
296 } else if let Some(ref check) = item.condition_check {
297 execute_condition_check(storage, check).await
298 } else {
299 Err(DynoxideError::ValidationException(
300 "TransactItem must contain exactly one of Put, Update, Delete, or ConditionCheck"
301 .to_string(),
302 ))
303 }
304}
305
306fn validate_eav_nesting(values: &Option<HashMap<String, AttributeValue>>) -> Result<()> {
310 if let Some(map) = values {
311 for value in map.values() {
312 crate::validation::validate_nesting_depth(value)?;
313 }
314 }
315 Ok(())
316}
317
318async fn execute_put<S: StorageBackend>(storage: &S, put: &TransactPut) -> Result<()> {
319 crate::validation::validate_table_name(&put.table_name)?;
320 let meta = helpers::require_table_for_item_op(storage, &put.table_name).await?;
321 let key_schema = helpers::parse_key_schema(&meta)?;
322
323 helpers::validate_item_keys(&put.item, &key_schema, &meta)?;
324 crate::validation::validate_item_attribute_values(&put.item)?;
325
326 let mut item = put.item.clone();
328 crate::validation::normalize_item_sets(&mut item);
329
330 let size = types::item_size(&item);
331 if size > types::MAX_ITEM_SIZE {
332 return Err(DynoxideError::ValidationException(
333 "Item size has exceeded the maximum allowed size".to_string(),
334 ));
335 }
336
337 let (pk, sk) = helpers::extract_key_strings(&item, &key_schema)?;
339
340 validate_eav_nesting(&put.expression_attribute_values)?;
341
342 let tracker = crate::expressions::TrackedExpressionAttributes::new(
343 &put.expression_attribute_names,
344 &put.expression_attribute_values,
345 );
346
347 if let Some(ref cond_expr) = put.condition_expression {
349 if let Ok(parsed) = crate::expressions::condition::parse(cond_expr) {
350 tracker.track_condition_expr(&parsed);
351 }
352 }
353
354 if let Some(ref cond_expr) = put.condition_expression {
356 let existing_json = storage.get_item(&put.table_name, &pk, &sk).await?;
357 let existing_item: Item = existing_json
358 .as_ref()
359 .and_then(|j| serde_json::from_str(j).ok())
360 .unwrap_or_default();
361
362 let return_item = if put.return_values_on_condition_check_failure.as_deref()
363 == Some("ALL_OLD")
364 && !existing_item.is_empty()
365 {
366 Some(existing_item.clone())
367 } else {
368 None
369 };
370 check_condition_tracked(cond_expr, &existing_item, &tracker, return_item)?;
371 }
372
373 tracker.check_unused()?;
374
375 let item_json = serde_json::to_string(&item)
376 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
377 let hash_prefix = item
378 .get(&key_schema.partition_key)
379 .map(crate::storage::compute_hash_prefix)
380 .unwrap_or_default();
381 let old_json = storage
382 .put_item_with_hash(&put.table_name, &pk, &sk, &item_json, size, &hash_prefix)
383 .await?;
384
385 let _ = super::gsi::maintain_gsis_after_write(
386 storage,
387 &put.table_name,
388 &meta,
389 &pk,
390 &sk,
391 &item,
392 &key_schema.partition_key,
393 key_schema.sort_key.as_deref(),
394 )
395 .await?;
396
397 super::lsi::maintain_lsis_after_write(
398 storage,
399 &put.table_name,
400 &meta,
401 &pk,
402 &sk,
403 &item,
404 &key_schema.partition_key,
405 key_schema.sort_key.as_deref(),
406 )
407 .await?;
408
409 let old_item: Option<Item> = old_json.and_then(|j| serde_json::from_str(&j).ok());
411 crate::streams::record_stream_event(storage, &meta, old_item.as_ref(), Some(&item)).await?;
412
413 Ok(())
414}
415
416async fn execute_update<S: StorageBackend>(storage: &S, update: &TransactUpdate) -> Result<()> {
417 crate::validation::validate_table_name(&update.table_name)?;
418 let meta = helpers::require_table_for_item_op(storage, &update.table_name).await?;
419 let key_schema = helpers::parse_key_schema(&meta)?;
420
421 helpers::validate_key_only(&update.key, &key_schema)?;
422 let (pk, sk) = helpers::extract_key_strings(&update.key, &key_schema)?;
424
425 let existing_json = storage.get_item(&update.table_name, &pk, &sk).await?;
426 let existing_item: Item = existing_json
427 .as_ref()
428 .and_then(|j| serde_json::from_str(j).ok())
429 .unwrap_or_default();
430
431 validate_eav_nesting(&update.expression_attribute_values)?;
432
433 let tracker = crate::expressions::TrackedExpressionAttributes::new(
434 &update.expression_attribute_names,
435 &update.expression_attribute_values,
436 );
437
438 if let Some(ref cond_expr) = update.condition_expression {
440 if let Ok(parsed) = crate::expressions::condition::parse(cond_expr) {
441 tracker.track_condition_expr(&parsed);
442 }
443 }
444 if let Ok(parsed) = crate::expressions::update::parse(&update.update_expression) {
445 tracker.track_update_expr(&parsed);
446 }
447
448 if let Some(ref cond_expr) = update.condition_expression {
452 let return_item = if update.return_values_on_condition_check_failure.as_deref()
453 == Some("ALL_OLD")
454 && existing_json.is_some()
455 {
456 Some(existing_item.clone())
457 } else {
458 None
459 };
460 check_condition_tracked(cond_expr, &existing_item, &tracker, return_item)?;
461 }
462
463 let mut item = existing_item;
466 if existing_json.is_none() {
467 for (k, v) in &update.key {
468 item.insert(k.clone(), v.clone());
469 }
470 }
471 let before_item = item.clone();
472
473 let parsed = crate::expressions::update::parse(&update.update_expression)
475 .map_err(DynoxideError::ValidationException)?;
476 crate::expressions::update::apply(&mut item, &parsed, &tracker)
477 .map_err(DynoxideError::ValidationException)?;
478
479 tracker.check_unused()?;
480
481 crate::validation::validate_item_attribute_values(&item)?;
483 crate::validation::normalize_item_sets(&mut item);
484
485 helpers::validate_updated_index_keys(&before_item, &item, &meta)?;
487
488 let size = types::item_size(&item);
489 if size > types::MAX_ITEM_SIZE {
490 return Err(DynoxideError::ValidationException(
491 "Item size has exceeded the maximum allowed size".to_string(),
492 ));
493 }
494
495 let old_for_stream = existing_json.clone();
497
498 let item_json = serde_json::to_string(&item)
499 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
500 let hash_prefix = update
501 .key
502 .get(&key_schema.partition_key)
503 .map(crate::storage::compute_hash_prefix)
504 .unwrap_or_default();
505 storage
506 .put_item_with_hash(&update.table_name, &pk, &sk, &item_json, size, &hash_prefix)
507 .await?;
508
509 let _ = super::gsi::maintain_gsis_after_write(
510 storage,
511 &update.table_name,
512 &meta,
513 &pk,
514 &sk,
515 &item,
516 &key_schema.partition_key,
517 key_schema.sort_key.as_deref(),
518 )
519 .await?;
520
521 super::lsi::maintain_lsis_after_write(
522 storage,
523 &update.table_name,
524 &meta,
525 &pk,
526 &sk,
527 &item,
528 &key_schema.partition_key,
529 key_schema.sort_key.as_deref(),
530 )
531 .await?;
532
533 let old_item: Option<Item> = old_for_stream.and_then(|j| serde_json::from_str(&j).ok());
535 crate::streams::record_stream_event(storage, &meta, old_item.as_ref(), Some(&item)).await?;
536
537 Ok(())
538}
539
540async fn execute_delete<S: StorageBackend>(storage: &S, delete: &TransactDelete) -> Result<()> {
541 crate::validation::validate_table_name(&delete.table_name)?;
542 let meta = helpers::require_table_for_item_op(storage, &delete.table_name).await?;
543 let key_schema = helpers::parse_key_schema(&meta)?;
544
545 helpers::validate_key_only(&delete.key, &key_schema)?;
546 let (pk, sk) = helpers::extract_key_strings(&delete.key, &key_schema)?;
548
549 validate_eav_nesting(&delete.expression_attribute_values)?;
550
551 let tracker = crate::expressions::TrackedExpressionAttributes::new(
552 &delete.expression_attribute_names,
553 &delete.expression_attribute_values,
554 );
555
556 if let Some(ref cond_expr) = delete.condition_expression {
558 if let Ok(parsed) = crate::expressions::condition::parse(cond_expr) {
559 tracker.track_condition_expr(&parsed);
560 }
561 }
562
563 if let Some(ref cond_expr) = delete.condition_expression {
565 let existing_json = storage.get_item(&delete.table_name, &pk, &sk).await?;
566 let existing_item: Item = existing_json
567 .as_ref()
568 .and_then(|j| serde_json::from_str(j).ok())
569 .unwrap_or_default();
570
571 let return_item = if delete.return_values_on_condition_check_failure.as_deref()
572 == Some("ALL_OLD")
573 && !existing_item.is_empty()
574 {
575 Some(existing_item.clone())
576 } else {
577 None
578 };
579 check_condition_tracked(cond_expr, &existing_item, &tracker, return_item)?;
580 }
581
582 tracker.check_unused()?;
583
584 let old_json = storage.delete_item(&delete.table_name, &pk, &sk).await?;
585 let _ = super::gsi::maintain_gsis_after_delete(storage, &delete.table_name, &meta, &pk, &sk)
586 .await?;
587 super::lsi::maintain_lsis_after_delete(storage, &delete.table_name, &meta, &pk, &sk).await?;
588
589 let old_item: Option<Item> = old_json.and_then(|j| serde_json::from_str(&j).ok());
591 if old_item.is_some() {
592 crate::streams::record_stream_event(storage, &meta, old_item.as_ref(), None).await?;
593 }
594
595 Ok(())
596}
597
598async fn execute_condition_check<S: StorageBackend>(
599 storage: &S,
600 check: &TransactConditionCheck,
601) -> Result<()> {
602 crate::validation::validate_table_name(&check.table_name)?;
603 let meta = helpers::require_table_for_item_op(storage, &check.table_name).await?;
604 let key_schema = helpers::parse_key_schema(&meta)?;
605
606 helpers::validate_key_only(&check.key, &key_schema)?;
607 let (pk, sk) = helpers::extract_key_strings(&check.key, &key_schema)?;
609
610 let existing_json = storage.get_item(&check.table_name, &pk, &sk).await?;
611 let existing_item: Item = existing_json
612 .as_ref()
613 .and_then(|j| serde_json::from_str(j).ok())
614 .unwrap_or_default();
615
616 validate_eav_nesting(&check.expression_attribute_values)?;
617
618 let tracker = crate::expressions::TrackedExpressionAttributes::new(
619 &check.expression_attribute_names,
620 &check.expression_attribute_values,
621 );
622
623 if let Ok(parsed) = crate::expressions::condition::parse(&check.condition_expression) {
625 tracker.track_condition_expr(&parsed);
626 }
627
628 let return_item = if check.return_values_on_condition_check_failure.as_deref()
629 == Some("ALL_OLD")
630 && !existing_item.is_empty()
631 {
632 Some(existing_item.clone())
633 } else {
634 None
635 };
636 check_condition_tracked(
637 &check.condition_expression,
638 &existing_item,
639 &tracker,
640 return_item,
641 )?;
642
643 tracker.check_unused()?;
644 Ok(())
645}
646
647fn check_condition_tracked(
648 expression: &str,
649 item: &Item,
650 tracker: &crate::expressions::TrackedExpressionAttributes,
651 return_item_on_failure: Option<Item>,
652) -> Result<()> {
653 let parsed = crate::expressions::condition::parse(expression)
654 .map_err(DynoxideError::ValidationException)?;
655 let result = crate::expressions::condition::evaluate(&parsed, item, tracker)
656 .map_err(DynoxideError::ValidationException)?;
657 if !result {
658 return Err(DynoxideError::ConditionalCheckFailedException(
659 "The conditional request failed".to_string(),
660 return_item_on_failure,
661 ));
662 }
663 Ok(())
664}
665
666fn get_action_table_and_size(item: &TransactWriteItem) -> (String, usize) {
672 if let Some(ref put) = item.put {
673 (put.table_name.clone(), types::item_size(&put.item))
674 } else if let Some(ref update) = item.update {
675 let key_size = types::item_size(&update.key);
676 let eav_size = update
677 .expression_attribute_values
678 .as_ref()
679 .map(|vals| vals.values().map(|v| v.size()).sum::<usize>())
680 .unwrap_or(0);
681 (update.table_name.clone(), key_size + eav_size)
682 } else if let Some(ref delete) = item.delete {
683 (delete.table_name.clone(), types::item_size(&delete.key))
684 } else if let Some(ref check) = item.condition_check {
685 (check.table_name.clone(), types::item_size(&check.key))
686 } else {
687 (String::new(), 0)
688 }
689}
690
691async fn target_for<S: StorageBackend>(
695 storage: &S,
696 table_name: &str,
697 key_source: &HashMap<String, AttributeValue>,
698) -> Result<Option<String>> {
699 crate::validation::validate_table_name(table_name)?;
700 let meta = helpers::require_table_for_item_op(storage, table_name).await?;
701 let key_schema = helpers::parse_key_schema(&meta)?;
702 match helpers::extract_key_strings(key_source, &key_schema) {
703 Ok((pk, sk)) => Ok(Some(format!("{table_name}#{pk}#{sk}"))),
704 Err(_) => Ok(None),
705 }
706}
707
708async fn get_item_target<S: StorageBackend>(
711 storage: &S,
712 item: &TransactWriteItem,
713) -> Result<Option<String>> {
714 if let Some(ref put) = item.put {
715 target_for(storage, &put.table_name, &put.item).await
716 } else if let Some(ref update) = item.update {
717 target_for(storage, &update.table_name, &update.key).await
718 } else if let Some(ref delete) = item.delete {
719 target_for(storage, &delete.table_name, &delete.key).await
720 } else if let Some(ref check) = item.condition_check {
721 target_for(storage, &check.table_name, &check.key).await
722 } else {
723 Err(DynoxideError::ValidationException(
724 "TransactItem must contain exactly one action".to_string(),
725 ))
726 }
727}