use crate::actions::helpers;
use crate::actions::index_capacity::{WriteCapacity, aggregate_by_table, per_table_capacity};
use crate::errors::{CancellationReason, DynoxideError, Result};
use crate::storage_backend::StorageBackend;
use crate::types::{self, AttributeValue, Item};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct TransactWriteItemsRequest {
#[serde(rename = "TransactItems")]
pub transact_items: Vec<TransactWriteItem>,
#[serde(rename = "ClientRequestToken", default)]
pub client_request_token: Option<String>,
#[serde(rename = "ReturnConsumedCapacity", default)]
pub return_consumed_capacity: Option<String>,
#[serde(rename = "ReturnItemCollectionMetrics", default)]
pub return_item_collection_metrics: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct TransactWriteItem {
#[serde(rename = "Put", default)]
pub put: Option<TransactPut>,
#[serde(rename = "Update", default)]
pub update: Option<TransactUpdate>,
#[serde(rename = "Delete", default)]
pub delete: Option<TransactDelete>,
#[serde(rename = "ConditionCheck", default)]
pub condition_check: Option<TransactConditionCheck>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct TransactPut {
#[serde(rename = "TableName")]
pub table_name: String,
#[serde(rename = "Item")]
pub item: Item,
#[serde(rename = "ConditionExpression", default)]
pub condition_expression: Option<String>,
#[serde(rename = "ExpressionAttributeNames", default)]
pub expression_attribute_names: Option<HashMap<String, String>>,
#[serde(
rename = "ExpressionAttributeValues",
default,
deserialize_with = "crate::types::deserialize_older_expression_values"
)]
pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
#[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
pub return_values_on_condition_check_failure: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct TransactUpdate {
#[serde(rename = "TableName")]
pub table_name: String,
#[serde(rename = "Key")]
pub key: HashMap<String, AttributeValue>,
#[serde(rename = "UpdateExpression")]
pub update_expression: String,
#[serde(rename = "ConditionExpression", default)]
pub condition_expression: Option<String>,
#[serde(rename = "ExpressionAttributeNames", default)]
pub expression_attribute_names: Option<HashMap<String, String>>,
#[serde(
rename = "ExpressionAttributeValues",
default,
deserialize_with = "crate::types::deserialize_older_expression_values"
)]
pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
#[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
pub return_values_on_condition_check_failure: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct TransactDelete {
#[serde(rename = "TableName")]
pub table_name: String,
#[serde(rename = "Key")]
pub key: HashMap<String, AttributeValue>,
#[serde(rename = "ConditionExpression", default)]
pub condition_expression: Option<String>,
#[serde(rename = "ExpressionAttributeNames", default)]
pub expression_attribute_names: Option<HashMap<String, String>>,
#[serde(
rename = "ExpressionAttributeValues",
default,
deserialize_with = "crate::types::deserialize_older_expression_values"
)]
pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
#[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
pub return_values_on_condition_check_failure: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct TransactConditionCheck {
#[serde(rename = "TableName")]
pub table_name: String,
#[serde(rename = "Key")]
pub key: HashMap<String, AttributeValue>,
#[serde(rename = "ConditionExpression")]
pub condition_expression: String,
#[serde(rename = "ExpressionAttributeNames", default)]
pub expression_attribute_names: Option<HashMap<String, String>>,
#[serde(
rename = "ExpressionAttributeValues",
default,
deserialize_with = "crate::types::deserialize_older_expression_values"
)]
pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
#[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
pub return_values_on_condition_check_failure: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct TransactWriteItemsResponse {
#[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
pub consumed_capacity: Option<Vec<crate::types::ConsumedCapacity>>,
#[serde(
rename = "ItemCollectionMetrics",
skip_serializing_if = "Option::is_none"
)]
pub item_collection_metrics: Option<HashMap<String, Vec<crate::types::ItemCollectionMetrics>>>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct CachedWrite {
pub(crate) response: TransactWriteItemsResponse,
#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
pub(crate) replay_sizes: Vec<(String, usize)>,
}
pub async fn execute<S: StorageBackend>(
storage: &S,
request: TransactWriteItemsRequest,
) -> Result<TransactWriteItemsResponse> {
Ok(execute_cached(storage, request).await?.response)
}
fn empty_name_in_write_action(item: &TransactWriteItem) -> Result<()> {
let (attributes, names, values) = if let Some(ref put) = item.put {
(
&put.item,
put.expression_attribute_names.as_ref(),
put.expression_attribute_values.as_ref(),
)
} else if let Some(ref update) = item.update {
(
&update.key,
update.expression_attribute_names.as_ref(),
update.expression_attribute_values.as_ref(),
)
} else if let Some(ref delete) = item.delete {
(
&delete.key,
delete.expression_attribute_names.as_ref(),
delete.expression_attribute_values.as_ref(),
)
} else if let Some(ref check) = item.condition_check {
(
&check.key,
check.expression_attribute_names.as_ref(),
check.expression_attribute_values.as_ref(),
)
} else {
return Ok(());
};
if crate::empty_names::map_has_empty_name(attributes) {
return Err(crate::empty_names::bare());
}
crate::empty_names::check_names(names)?;
if let Some(key) = crate::empty_names::value_placeholder(values, None) {
return Err(DynoxideError::ValidationException(
crate::empty_names::value_placeholder_message(key),
));
}
Ok(())
}
fn items_echo(items: &[TransactWriteItem]) -> String {
let mut out = crate::actions::batch_write_item::BoundedEcho::new();
let _ = write_items(&mut out, items);
out.finish()
}
fn write_items(out: &mut dyn std::fmt::Write, items: &[TransactWriteItem]) -> std::fmt::Result {
use crate::actions::batch_write_item::{
write_java_item, write_java_names, write_java_text, write_java_values,
};
let conditions = |out: &mut dyn std::fmt::Write,
condition: Option<&str>,
names: Option<&HashMap<String, String>>,
values: Option<&HashMap<String, AttributeValue>>,
return_values: Option<&str>|
-> std::fmt::Result {
out.write_str(", conditionExpression=")?;
write_java_text(out, condition)?;
out.write_str(", expressionAttributeNames=")?;
write_java_names(out, names)?;
out.write_str(", expressionAttributeValues=")?;
write_java_values(out, values)?;
out.write_str(", returnValuesOnConditionCheckFailure=")?;
write_java_text(out, return_values)?;
out.write_str(", workloadProfileName=null)")
};
out.write_str("[")?;
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.write_str(", ")?;
}
out.write_str("TransactWriteItem(conditionCheck=")?;
match item.condition_check {
Some(ref check) => {
out.write_str("ConditionCheck(key=")?;
write_java_item(out, &check.key)?;
write!(out, ", tableName={}", check.table_name)?;
conditions(
out,
Some(&check.condition_expression),
check.expression_attribute_names.as_ref(),
check.expression_attribute_values.as_ref(),
check.return_values_on_condition_check_failure.as_deref(),
)?;
}
None => out.write_str("null")?,
}
out.write_str(", put=")?;
match item.put {
Some(ref put) => {
out.write_str("Put(item=")?;
write_java_item(out, &put.item)?;
write!(out, ", tableName={}", put.table_name)?;
conditions(
out,
put.condition_expression.as_deref(),
put.expression_attribute_names.as_ref(),
put.expression_attribute_values.as_ref(),
put.return_values_on_condition_check_failure.as_deref(),
)?;
}
None => out.write_str("null")?,
}
out.write_str(", delete=")?;
match item.delete {
Some(ref delete) => {
out.write_str("Delete(key=")?;
write_java_item(out, &delete.key)?;
write!(out, ", tableName={}", delete.table_name)?;
conditions(
out,
delete.condition_expression.as_deref(),
delete.expression_attribute_names.as_ref(),
delete.expression_attribute_values.as_ref(),
delete.return_values_on_condition_check_failure.as_deref(),
)?;
}
None => out.write_str("null")?,
}
out.write_str(", update=")?;
match item.update {
Some(ref update) => {
out.write_str("Update(key=")?;
write_java_item(out, &update.key)?;
write!(
out,
", updateExpression={}, tableName={}",
update.update_expression, update.table_name
)?;
conditions(
out,
update.condition_expression.as_deref(),
update.expression_attribute_names.as_ref(),
update.expression_attribute_values.as_ref(),
update.return_values_on_condition_check_failure.as_deref(),
)?;
}
None => out.write_str("null")?,
}
out.write_str(")")?;
}
out.write_str("]")
}
pub(crate) fn check_request(request: &TransactWriteItemsRequest) -> Result<()> {
let items = &request.transact_items;
if items.is_empty() {
return Err(DynoxideError::ValidationException(
"1 validation error detected: Value '[]' at 'transactItems' failed to satisfy constraint: Member must have length greater than or equal to 1".to_string(),
));
}
if items.len() > 100 {
return Err(DynoxideError::ValidationException(format!(
"1 validation error detected: Value '{}' at 'transactItems' failed to satisfy constraint: Member must have length less than or equal to 100",
items_echo(items)
)));
}
let mut member_errors: Vec<String> = Vec::new();
for (position, item) in items.iter().enumerate() {
let actions = [
(
"put",
item.put
.as_ref()
.map(|a| &a.return_values_on_condition_check_failure),
),
(
"update",
item.update
.as_ref()
.map(|a| &a.return_values_on_condition_check_failure),
),
(
"delete",
item.delete
.as_ref()
.map(|a| &a.return_values_on_condition_check_failure),
),
(
"conditionCheck",
item.condition_check
.as_ref()
.map(|a| &a.return_values_on_condition_check_failure),
),
];
for (action, value) in actions {
if let Some(msg) = value.and_then(|value| {
crate::validation::return_values_on_condition_check_failure_rejection(
value.as_deref(),
&format!(
"transactItems.{}.member.{action}.returnValuesOnConditionCheckFailure",
position + 1
),
)
}) {
member_errors.push(msg);
}
}
}
member_errors.extend(
[
crate::token_constraint_error(request.client_request_token.as_deref()),
crate::validation::return_consumed_capacity_rejection(
request.return_consumed_capacity.as_deref(),
),
crate::validation::return_item_collection_metrics_rejection(
request.return_item_collection_metrics.as_deref(),
),
]
.into_iter()
.flatten(),
);
if let Some(msg) = crate::validation::format_validation_errors(&member_errors) {
return Err(DynoxideError::ValidationException(msg));
}
for item in items {
let actions = [
item.put.is_some(),
item.update.is_some(),
item.delete.is_some(),
item.condition_check.is_some(),
]
.into_iter()
.filter(|&present| present)
.count();
match actions {
0 => {
return Err(DynoxideError::ValidationException(
"Invalid Request: TransactWriteRequest should contain Delete or Put or Update request"
.to_string(),
));
}
1 => {}
_ => {
return Err(DynoxideError::ValidationException(
"TransactItems can only contain one of Check, Put, Update or Delete"
.to_string(),
));
}
}
}
let mut name_errors = Vec::new();
for (position, item) in items.iter().enumerate() {
let (action, table_name) = if let Some(ref put) = item.put {
("put", &put.table_name)
} else if let Some(ref update) = item.update {
("update", &update.table_name)
} else if let Some(ref delete) = item.delete {
("delete", &delete.table_name)
} else if let Some(ref check) = item.condition_check {
("conditionCheck", &check.table_name)
} else {
continue;
};
name_errors.extend(crate::validation::member_table_name_errors(
table_name,
&format!("transactItems.{}.member.{action}.tableName", position + 1),
));
}
if let Some(msg) = crate::validation::format_validation_errors(&name_errors) {
return Err(DynoxideError::ValidationException(msg));
}
Ok(())
}
pub(crate) async fn execute_cached<S: StorageBackend>(
storage: &S,
request: TransactWriteItemsRequest,
) -> Result<CachedWrite> {
check_request(&request)?;
let items = &request.transact_items;
for item in items {
empty_name_in_write_action(item)?;
}
for item in items {
validate_action_request(item)?;
}
for item in items {
let key = item
.delete
.as_ref()
.map(|d| &d.key)
.or(item.condition_check.as_ref().map(|c| &c.key))
.or(item.update.as_ref().map(|u| &u.key));
if let Some(key) = key {
crate::value_nesting::check_key(key)?;
}
}
let mut seen_targets = HashSet::new();
for item in items {
if let Some(target) = get_item_target(storage, item).await?
&& !seen_targets.insert(target)
{
return Err(DynoxideError::ValidationException(
"Transaction request cannot include multiple operations on one item".to_string(),
));
}
}
let total_size: usize = items.iter().map(|i| get_action_table_and_size(i).1).sum();
if total_size > 4 * 1024 * 1024 {
return Err(DynoxideError::ValidationException(
"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(),
));
}
let capacity_mode = request.return_consumed_capacity.as_deref();
let capacity = helpers::with_write_transaction(
storage,
execute_within_transaction(storage, items, capacity_mode),
)
.await?;
Ok(CachedWrite {
response: TransactWriteItemsResponse {
consumed_capacity: build_write_capacity(&capacity, &request.return_consumed_capacity),
item_collection_metrics: None,
},
#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
replay_sizes: replay_sizes(&capacity),
})
}
#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
fn replay_sizes(capacity: &[WriteCapacity]) -> Vec<(String, usize)> {
capacity
.iter()
.map(|record| {
let larger = record
.old_size
.unwrap_or(0)
.max(record.new_size.unwrap_or(0));
(record.table_name.clone(), larger)
})
.collect()
}
fn build_write_capacity(
capacity: &[WriteCapacity],
mode: &Option<String>,
) -> Option<Vec<crate::types::ConsumedCapacity>> {
if !matches!(mode.as_deref(), Some("TOTAL") | Some("INDEXES")) {
return None;
}
let by_table = aggregate_by_table(capacity, crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
per_table_capacity(
&by_table,
mode,
crate::types::transactional_write_capacity_with_indexes,
)
}
#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
pub(crate) fn replay_response(cached: &CachedWrite, mode: &Option<String>) -> CachedWrite {
CachedWrite {
response: TransactWriteItemsResponse {
consumed_capacity: crate::types::build_transactional_capacity(
&crate::actions::index_capacity::transactional_read_units(&cached.replay_sizes),
mode,
crate::types::transactional_read_capacity,
),
item_collection_metrics: cached.response.item_collection_metrics.clone(),
},
replay_sizes: cached.replay_sizes.clone(),
}
}
async fn execute_within_transaction<S: StorageBackend>(
storage: &S,
items: &[TransactWriteItem],
capacity_mode: Option<&str>,
) -> Result<Vec<WriteCapacity>> {
let mut cancellation_reasons: Vec<CancellationReason> = Vec::with_capacity(items.len());
let mut capacity: Vec<WriteCapacity> = Vec::with_capacity(items.len());
let mut has_failure = false;
for item in items {
let reason = execute_single_action(storage, item, capacity_mode).await;
match reason {
Ok(action_capacity) => {
capacity.push(action_capacity);
cancellation_reasons.push(CancellationReason {
code: "None".to_string(),
message: None,
item: None,
});
}
Err(e) => {
if matches!(e, DynoxideError::KeyEmptyValueValidation(_)) {
return Err(e);
}
let message = Some(e.to_string());
let (code, item) = match e {
DynoxideError::ConditionalCheckFailedException(_, item) => {
("ConditionalCheckFailed".to_string(), item)
}
DynoxideError::ValidationException(_) => ("ValidationError".to_string(), None),
storage_failure => return Err(storage_failure),
};
has_failure = true;
cancellation_reasons.push(CancellationReason {
code,
message,
item,
});
}
}
}
if has_failure {
let codes: Vec<&str> = cancellation_reasons
.iter()
.map(|r| r.code.as_str())
.collect();
let message = format!(
"Transaction cancelled, please refer cancellation reasons for specific reasons [{}]",
codes.join(", ")
);
return Err(DynoxideError::TransactionCanceledException(
message,
cancellation_reasons,
));
}
Ok(capacity)
}
async fn execute_single_action<S: StorageBackend>(
storage: &S,
item: &TransactWriteItem,
capacity_mode: Option<&str>,
) -> Result<WriteCapacity> {
if let Some(ref put) = item.put {
execute_put(storage, put, capacity_mode).await
} else if let Some(ref update) = item.update {
execute_update(storage, update, capacity_mode).await
} else if let Some(ref delete) = item.delete {
execute_delete(storage, delete, capacity_mode).await
} else if let Some(ref check) = item.condition_check {
execute_condition_check(storage, check).await
} else {
Err(DynoxideError::ValidationException(
"TransactItem must contain exactly one of Put, Update, Delete, or ConditionCheck"
.to_string(),
))
}
}
fn validate_action_request(item: &TransactWriteItem) -> Result<()> {
validate_action_sets(item)?;
if let Some(ref put) = item.put {
validate_expression_value_depth(&put.expression_attribute_values)?;
for value in put.item.values() {
crate::validation::validate_nesting_depth(value)?;
}
validate_condition_size(put.condition_expression.as_deref())?;
if types::item_size(&put.item) > types::MAX_ITEM_SIZE {
return Err(DynoxideError::ValidationException(
"Item size has exceeded the maximum allowed size".to_string(),
));
}
validate_condition_syntax(put.condition_expression.as_deref())?;
validate_expression_references(
&put.expression_attribute_names,
&put.expression_attribute_values,
put.condition_expression.as_deref(),
None,
)?;
} else if let Some(ref update) = item.update {
validate_expression_value_depth(&update.expression_attribute_values)?;
if update.update_expression.len() > crate::expressions::MAX_EXPRESSION_BYTES {
return Err(DynoxideError::ValidationException(
"Invalid UpdateExpression: Expression size has exceeded the maximum allowed size;"
.to_string(),
));
}
if update.update_expression.is_empty() {
return Err(DynoxideError::ValidationException(
"Invalid UpdateExpression: The expression can not be empty;".to_string(),
));
}
let update_expression = crate::expressions::update::parse(&update.update_expression)
.map_err(DynoxideError::ValidationException)?;
validate_condition_size(update.condition_expression.as_deref())?;
validate_condition_syntax(update.condition_expression.as_deref())?;
validate_expression_references(
&update.expression_attribute_names,
&update.expression_attribute_values,
update.condition_expression.as_deref(),
Some(&update_expression),
)?;
} else if let Some(ref delete) = item.delete {
validate_expression_value_depth(&delete.expression_attribute_values)?;
validate_condition_size(delete.condition_expression.as_deref())?;
validate_condition_syntax(delete.condition_expression.as_deref())?;
validate_expression_references(
&delete.expression_attribute_names,
&delete.expression_attribute_values,
delete.condition_expression.as_deref(),
None,
)?;
} else if let Some(ref check) = item.condition_check {
validate_expression_value_depth(&check.expression_attribute_values)?;
validate_condition_size(Some(&check.condition_expression))?;
validate_condition_syntax(Some(&check.condition_expression))?;
validate_expression_references(
&check.expression_attribute_names,
&check.expression_attribute_values,
Some(&check.condition_expression),
None,
)?;
}
Ok(())
}
fn validate_action_sets(item: &TransactWriteItem) -> Result<()> {
use crate::validation::{ValueLayer, find_set_error};
let (values, values_map) = if let Some(ref put) = item.put {
(&put.item, &put.expression_attribute_values)
} else if let Some(ref update) = item.update {
(&update.key, &update.expression_attribute_values)
} else if let Some(ref delete) = item.delete {
(&delete.key, &delete.expression_attribute_values)
} else if let Some(ref check) = item.condition_check {
(&check.key, &check.expression_attribute_values)
} else {
return Ok(());
};
let mut names: Vec<&String> = values.keys().collect();
names.sort();
for name in names {
if let Some(message) = find_set_error(&values[name], ValueLayer::Older) {
return Err(DynoxideError::ValidationException(message));
}
}
if let Some(eav) = values_map {
let mut keys: Vec<&String> = eav.keys().collect();
keys.sort();
for key in keys {
if let Some(message) = find_set_error(&eav[key], ValueLayer::Older) {
return Err(DynoxideError::ValidationException(format!(
"ExpressionAttributeValues contains invalid value: {message} for key {key}"
)));
}
}
}
Ok(())
}
fn validate_expression_references(
names: &Option<HashMap<String, String>>,
values: &Option<HashMap<String, AttributeValue>>,
condition: Option<&str>,
update: Option<&crate::expressions::update::UpdateExpr>,
) -> Result<()> {
if condition.is_none() && update.is_none() {
return Ok(());
}
let tracker = crate::expressions::TrackedExpressionAttributes::new(names, values);
if let Some(update) = update {
tracker.track_update_expr(update);
crate::expressions::update::check_target_overlaps(update, &tracker)
.map_err(DynoxideError::ValidationException)?;
}
if let Some(condition) = condition {
let parsed = crate::expressions::condition::parse(condition).map_err(|e| {
DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
})?;
crate::expressions::condition::track_references(&parsed, &tracker).map_err(|e| {
DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
})?;
}
tracker.check_unused()
}
const MAX_EXPRESSION_VALUE_DEPTH: usize = 32;
fn validate_expression_value_depth(values: &Option<HashMap<String, AttributeValue>>) -> Result<()> {
let Some(values) = values else {
return Ok(());
};
let mut keys: Vec<&String> = values.keys().collect();
keys.sort();
for key in keys {
if crate::validation::nesting_depth(&values[key]) > MAX_EXPRESSION_VALUE_DEPTH {
return Err(DynoxideError::ValidationException(format!(
"ExpressionAttributeValues contains invalid value: Nesting Levels have exceeded \
supported limits: Attributes in the item have nested levels beyond supported \
limit for key {key}"
)));
}
}
Ok(())
}
fn validate_condition_syntax(expression: Option<&str>) -> Result<()> {
let Some(expr) = expression else {
return Ok(());
};
if expr.is_empty() {
return Err(DynoxideError::ValidationException(
"Invalid ConditionExpression: The expression can not be empty;".to_string(),
));
}
crate::expressions::condition::parse(expr).map_err(|e| {
DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
})?;
Ok(())
}
fn validate_condition_size(expression: Option<&str>) -> Result<()> {
if let Some(expr) = expression {
crate::expressions::check_expression_size_with_figure(expr).map_err(|e| {
DynoxideError::ValidationException(format!("Invalid ConditionExpression: {e}"))
})?;
}
Ok(())
}
async fn execute_put<S: StorageBackend>(
storage: &S,
put: &TransactPut,
capacity_mode: Option<&str>,
) -> Result<WriteCapacity> {
crate::validation::validate_table_name(&put.table_name)?;
let meta = helpers::require_table_for_item_op(storage, &put.table_name).await?;
let key_schema = helpers::parse_key_schema(&meta)?;
helpers::validate_item_keys(&put.item, &key_schema, &meta)?;
crate::validation::validate_item_attribute_values(&put.item)?;
let mut item = put.item.clone();
crate::validation::normalize_item_sets(&mut item);
let vector_defs = super::vector_index::parse_vector_defs(&meta)?;
let size = types::billed_item_size(&item, &vector_defs);
let (pk, sk) = helpers::extract_key_strings(&item, &key_schema)?;
let tracker = crate::expressions::TrackedExpressionAttributes::new(
&put.expression_attribute_names,
&put.expression_attribute_values,
);
if let Some(ref cond_expr) = put.condition_expression
&& let Ok(parsed) = crate::expressions::condition::parse(cond_expr)
{
tracker.track_condition_expr(&parsed);
}
if let Some(ref cond_expr) = put.condition_expression {
let existing_json = storage.get_item(&put.table_name, &pk, &sk).await?;
let existing_item: Item = existing_json
.as_ref()
.and_then(|j| serde_json::from_str(j).ok())
.unwrap_or_default();
let return_item = if put.return_values_on_condition_check_failure.as_deref()
== Some("ALL_OLD")
&& !existing_item.is_empty()
{
Some(existing_item.clone())
} else {
None
};
check_condition_tracked(cond_expr, &existing_item, &tracker, return_item)?;
}
if put.condition_expression.is_some() {
tracker.check_unused()?;
}
let item_json = serde_json::to_string(&item)
.map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
let hash_prefix = item
.get(&key_schema.partition_key)
.map(crate::storage::compute_hash_prefix)
.unwrap_or_default();
let old_json = storage
.put_item_with_hash(&put.table_name, &pk, &sk, &item_json, size, &hash_prefix)
.await?;
let old_item: Option<Item> = old_json.and_then(|j| serde_json::from_str(&j).ok());
let target = super::gsi::IndexWrite {
table_name: &put.table_name,
pk: &pk,
sk: &sk,
pk_attr: &key_schema.partition_key,
sk_attr: key_schema.sort_key.as_deref(),
old_item: old_item.as_ref(),
capacity_mode,
};
let gsi_units = super::gsi::maintain_gsis_after_write(storage, &meta, &target, &item).await?;
let lsi_units = super::lsi::maintain_lsis_after_write(storage, &meta, &target, &item).await?;
let vector_bytes =
super::vector_index::maintain_vector_indexes_after_write(storage, &meta, &target, &item)
.await?;
crate::streams::record_stream_event(storage, &meta, old_item.as_ref(), Some(&item)).await?;
Ok(WriteCapacity::new(
&put.table_name,
old_item
.as_ref()
.map(|old| types::billed_item_size(old, &vector_defs)),
Some(size),
gsi_units,
lsi_units,
)
.with_vector_bytes(vector_bytes))
}
async fn execute_update<S: StorageBackend>(
storage: &S,
update: &TransactUpdate,
capacity_mode: Option<&str>,
) -> Result<WriteCapacity> {
crate::validation::validate_table_name(&update.table_name)?;
let meta = helpers::require_table_for_item_op(storage, &update.table_name).await?;
let key_schema = helpers::parse_key_schema(&meta)?;
helpers::validate_key_only(&update.key, &key_schema)?;
let (pk, sk) = helpers::extract_key_strings(&update.key, &key_schema)?;
let existing_json = storage.get_item(&update.table_name, &pk, &sk).await?;
let existing_item: Item = existing_json
.as_ref()
.and_then(|j| serde_json::from_str(j).ok())
.unwrap_or_default();
let tracker = crate::expressions::TrackedExpressionAttributes::new(
&update.expression_attribute_names,
&update.expression_attribute_values,
);
if let Some(ref cond_expr) = update.condition_expression
&& let Ok(parsed) = crate::expressions::condition::parse(cond_expr)
{
tracker.track_condition_expr(&parsed);
}
if let Ok(parsed) = crate::expressions::update::parse(&update.update_expression) {
tracker.track_update_expr(&parsed);
}
if let Some(ref cond_expr) = update.condition_expression {
let return_item = if update.return_values_on_condition_check_failure.as_deref()
== Some("ALL_OLD")
&& existing_json.is_some()
{
Some(existing_item.clone())
} else {
None
};
check_condition_tracked(cond_expr, &existing_item, &tracker, return_item)?;
}
let mut item = existing_item;
if existing_json.is_none() {
for (k, v) in &update.key {
item.insert(k.clone(), v.clone());
}
}
let before_item = item.clone();
let parsed = crate::expressions::update::parse(&update.update_expression)
.map_err(DynoxideError::ValidationException)?;
crate::expressions::update::apply(&mut item, &parsed, &tracker)
.map_err(DynoxideError::ValidationException)?;
tracker.check_unused()?;
crate::validation::validate_updated_item_nesting(&item)?;
crate::validation::validate_item_attribute_values(&item)?;
crate::validation::normalize_item_sets(&mut item);
helpers::validate_updated_index_keys(&before_item, &item, &meta)?;
if types::item_size(&item) > types::MAX_ITEM_SIZE {
return Err(DynoxideError::ValidationException(
"Item size to update has exceeded the maximum allowed size".to_string(),
));
}
let vector_defs = super::vector_index::parse_vector_defs(&meta)?;
let size = types::billed_item_size(&item, &vector_defs);
let old_for_stream = existing_json.clone();
let item_json = serde_json::to_string(&item)
.map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
let hash_prefix = update
.key
.get(&key_schema.partition_key)
.map(crate::storage::compute_hash_prefix)
.unwrap_or_default();
storage
.put_item_with_hash(&update.table_name, &pk, &sk, &item_json, size, &hash_prefix)
.await?;
let old_item: Option<Item> = old_for_stream.and_then(|j| serde_json::from_str(&j).ok());
let target = super::gsi::IndexWrite {
table_name: &update.table_name,
pk: &pk,
sk: &sk,
pk_attr: &key_schema.partition_key,
sk_attr: key_schema.sort_key.as_deref(),
old_item: old_item.as_ref(),
capacity_mode,
};
let gsi_units = super::gsi::maintain_gsis_after_write(storage, &meta, &target, &item).await?;
let lsi_units = super::lsi::maintain_lsis_after_write(storage, &meta, &target, &item).await?;
let vector_bytes =
super::vector_index::maintain_vector_indexes_after_write(storage, &meta, &target, &item)
.await?;
crate::streams::record_stream_event(storage, &meta, old_item.as_ref(), Some(&item)).await?;
Ok(WriteCapacity::new(
&update.table_name,
old_item
.as_ref()
.map(|old| types::billed_item_size(old, &vector_defs)),
Some(size),
gsi_units,
lsi_units,
)
.with_vector_bytes(vector_bytes))
}
async fn execute_delete<S: StorageBackend>(
storage: &S,
delete: &TransactDelete,
capacity_mode: Option<&str>,
) -> Result<WriteCapacity> {
crate::validation::validate_table_name(&delete.table_name)?;
let meta = helpers::require_table_for_item_op(storage, &delete.table_name).await?;
let key_schema = helpers::parse_key_schema(&meta)?;
helpers::validate_key_only(&delete.key, &key_schema)?;
let (pk, sk) = helpers::extract_key_strings(&delete.key, &key_schema)?;
let tracker = crate::expressions::TrackedExpressionAttributes::new(
&delete.expression_attribute_names,
&delete.expression_attribute_values,
);
if let Some(ref cond_expr) = delete.condition_expression
&& let Ok(parsed) = crate::expressions::condition::parse(cond_expr)
{
tracker.track_condition_expr(&parsed);
}
if let Some(ref cond_expr) = delete.condition_expression {
let existing_json = storage.get_item(&delete.table_name, &pk, &sk).await?;
let existing_item: Item = existing_json
.as_ref()
.and_then(|j| serde_json::from_str(j).ok())
.unwrap_or_default();
let return_item = if delete.return_values_on_condition_check_failure.as_deref()
== Some("ALL_OLD")
&& !existing_item.is_empty()
{
Some(existing_item.clone())
} else {
None
};
check_condition_tracked(cond_expr, &existing_item, &tracker, return_item)?;
}
if delete.condition_expression.is_some() {
tracker.check_unused()?;
}
let old_json = storage.delete_item(&delete.table_name, &pk, &sk).await?;
let old_item: Option<Item> = old_json.and_then(|j| serde_json::from_str(&j).ok());
let target = super::gsi::IndexWrite {
table_name: &delete.table_name,
pk: &pk,
sk: &sk,
pk_attr: &key_schema.partition_key,
sk_attr: key_schema.sort_key.as_deref(),
old_item: old_item.as_ref(),
capacity_mode,
};
let gsi_units = super::gsi::maintain_gsis_after_delete(storage, &meta, &target).await?;
let lsi_units = super::lsi::maintain_lsis_after_delete(storage, &meta, &target).await?;
let vector_bytes =
super::vector_index::maintain_vector_indexes_after_delete(storage, &meta, &target).await?;
if old_item.is_some() {
crate::streams::record_stream_event(storage, &meta, old_item.as_ref(), None).await?;
}
let vector_defs = super::vector_index::parse_vector_defs(&meta)?;
Ok(WriteCapacity::new(
&delete.table_name,
old_item
.as_ref()
.map(|old| types::billed_item_size(old, &vector_defs)),
None,
gsi_units,
lsi_units,
)
.with_vector_bytes(vector_bytes))
}
async fn execute_condition_check<S: StorageBackend>(
storage: &S,
check: &TransactConditionCheck,
) -> Result<WriteCapacity> {
crate::validation::validate_table_name(&check.table_name)?;
let meta = helpers::require_table_for_item_op(storage, &check.table_name).await?;
let key_schema = helpers::parse_key_schema(&meta)?;
helpers::validate_key_only(&check.key, &key_schema)?;
let (pk, sk) = helpers::extract_key_strings(&check.key, &key_schema)?;
let existing_json = storage.get_item(&check.table_name, &pk, &sk).await?;
let existing_item: Item = existing_json
.as_ref()
.and_then(|j| serde_json::from_str(j).ok())
.unwrap_or_default();
let tracker = crate::expressions::TrackedExpressionAttributes::new(
&check.expression_attribute_names,
&check.expression_attribute_values,
);
if let Ok(parsed) = crate::expressions::condition::parse(&check.condition_expression) {
tracker.track_condition_expr(&parsed);
}
let return_item = if check.return_values_on_condition_check_failure.as_deref()
== Some("ALL_OLD")
&& !existing_item.is_empty()
{
Some(existing_item.clone())
} else {
None
};
check_condition_tracked(
&check.condition_expression,
&existing_item,
&tracker,
return_item,
)?;
tracker.check_unused()?;
Ok(WriteCapacity::condition_check(
&check.table_name,
existing_json.is_some().then_some(&existing_item),
&super::vector_index::parse_vector_defs(&meta)?,
))
}
fn check_condition_tracked(
expression: &str,
item: &Item,
tracker: &crate::expressions::TrackedExpressionAttributes,
return_item_on_failure: Option<Item>,
) -> Result<()> {
let parsed = crate::expressions::condition::parse(expression)
.map_err(DynoxideError::ValidationException)?;
let result = crate::expressions::condition::evaluate(&parsed, item, tracker)
.map_err(DynoxideError::ValidationException)?;
if !result {
return Err(DynoxideError::ConditionalCheckFailedException(
"The conditional request failed".to_string(),
return_item_on_failure,
));
}
Ok(())
}
fn get_action_table_and_size(item: &TransactWriteItem) -> (String, usize) {
if let Some(ref put) = item.put {
(put.table_name.clone(), types::item_size(&put.item))
} else if let Some(ref update) = item.update {
let key_size = types::item_size(&update.key);
let eav_size = update
.expression_attribute_values
.as_ref()
.map(|vals| vals.values().map(|v| v.size()).sum::<usize>())
.unwrap_or(0);
(update.table_name.clone(), key_size + eav_size)
} else if let Some(ref delete) = item.delete {
(delete.table_name.clone(), types::item_size(&delete.key))
} else if let Some(ref check) = item.condition_check {
(check.table_name.clone(), types::item_size(&check.key))
} else {
(String::new(), 0)
}
}
async fn target_for<S: StorageBackend>(
storage: &S,
table_name: &str,
key_source: &HashMap<String, AttributeValue>,
) -> Result<Option<String>> {
crate::validation::validate_table_name(table_name)?;
let meta = helpers::require_table_for_item_op(storage, table_name).await?;
let key_schema = helpers::parse_key_schema(&meta)?;
match helpers::extract_key_strings(key_source, &key_schema) {
Ok((pk, sk)) => Ok(Some(format!("{table_name}#{pk}#{sk}"))),
Err(_) => Ok(None),
}
}
async fn get_item_target<S: StorageBackend>(
storage: &S,
item: &TransactWriteItem,
) -> Result<Option<String>> {
if let Some(ref put) = item.put {
target_for(storage, &put.table_name, &put.item).await
} else if let Some(ref update) = item.update {
target_for(storage, &update.table_name, &update.key).await
} else if let Some(ref delete) = item.delete {
target_for(storage, &delete.table_name, &delete.key).await
} else if let Some(ref check) = item.condition_check {
target_for(storage, &check.table_name, &check.key).await
} else {
Err(DynoxideError::ValidationException(
"TransactItem must contain exactly one action".to_string(),
))
}
}