1use crate::actions::helpers;
2use crate::errors::{CancellationReason, DynoxideError, Result};
3use crate::partiql;
4use crate::storage_backend::StorageBackend;
5use crate::types::{AttributeValue, Item};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Default, Deserialize)]
9pub struct ExecuteTransactionRequest {
10 #[serde(rename = "TransactStatements")]
11 pub transact_statements: Vec<ParameterizedStatement>,
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}
17
18#[derive(Debug, Clone, Default, Deserialize, Serialize)]
22pub struct ParameterizedStatement {
23 #[serde(rename = "Statement")]
24 pub statement: String,
25 #[serde(rename = "Parameters", default)]
26 pub parameters: Option<Vec<AttributeValue>>,
27}
28
29#[derive(Debug, Clone, Default, Serialize)]
32pub struct ExecuteTransactionResponse {
33 #[serde(rename = "Responses", skip_serializing_if = "Option::is_none")]
34 pub responses: Option<Vec<ItemResponse>>,
35 #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
36 pub consumed_capacity: Option<Vec<crate::types::ConsumedCapacity>>,
37}
38
39#[derive(Debug, Clone, Default, Serialize)]
40pub struct ItemResponse {
41 #[serde(rename = "Item", skip_serializing_if = "Option::is_none")]
42 pub item: Option<Item>,
43}
44
45pub async fn execute<S: StorageBackend>(
46 storage: &S,
47 request: ExecuteTransactionRequest,
48) -> Result<ExecuteTransactionResponse> {
49 let statements = &request.transact_statements;
50
51 if statements.is_empty() {
53 return Err(DynoxideError::ValidationException(
54 "1 validation error detected: Value at 'transactStatements' failed to satisfy constraint: Member must have length greater than or equal to 1".to_string(),
55 ));
56 }
57 if statements.len() > 100 {
58 return Err(DynoxideError::ValidationException(
59 "Member must have length less than or equal to 100".to_string(),
60 ));
61 }
62
63 let mut parsed = Vec::with_capacity(statements.len());
65 for (index, stmt) in statements.iter().enumerate() {
66 if let Some(msg) = partiql::parser::count_projection_rejection(&stmt.statement) {
70 return Err(DynoxideError::ValidationException(msg));
71 }
72 let ast = partiql::parser::parse(&stmt.statement).map_err(|e| {
73 DynoxideError::ValidationException(format!(
74 "Statement wasn't well formed, can't be processed: {e}"
75 ))
76 })?;
77 if partiql::parser::returning_variant(&ast).is_some() {
81 return Err(DynoxideError::ValidationException(format!(
82 "Validation failed in TransactStatements[{index}]: RETURNING clause is not supported in ExecuteTransaction."
83 )));
84 }
85 let params = stmt.parameters.clone().unwrap_or_default();
86 parsed.push((ast, params));
87 }
88
89 let responses =
91 helpers::with_write_transaction(storage, execute_within_transaction(storage, &parsed))
92 .await?;
93
94 let builder = if is_read_set(&parsed) {
102 crate::types::transactional_read_capacity
103 } else {
104 crate::types::transactional_write_capacity
105 };
106 let consumed_capacity = crate::types::build_transactional_capacity(
107 &statement_table_units(parsed.iter().map(|(stmt, _)| stmt)),
108 &request.return_consumed_capacity,
109 builder,
110 );
111
112 Ok(ExecuteTransactionResponse {
113 responses: Some(responses),
114 consumed_capacity,
115 })
116}
117
118fn is_read_set(parsed: &[(partiql::parser::Statement, Vec<AttributeValue>)]) -> bool {
125 parsed
126 .iter()
127 .all(|(stmt, _)| matches!(stmt, partiql::parser::Statement::Select { .. }))
128}
129
130fn statement_table_units<'a>(
135 statements: impl Iterator<Item = &'a partiql::parser::Statement>,
136) -> std::collections::HashMap<String, f64> {
137 let mut table_units: std::collections::HashMap<String, f64> = std::collections::HashMap::new();
138 for stmt in statements {
139 if let Some(tbl) = partiql::parser::table_name(stmt) {
140 *table_units.entry(tbl.to_string()).or_default() +=
141 crate::types::TRANSACTIONAL_CAPACITY_FACTOR;
142 }
143 }
144 table_units
145}
146
147pub(crate) fn replay_response(
156 statements: &[ParameterizedStatement],
157 mode: &Option<String>,
158 cached_responses: Option<Vec<ItemResponse>>,
159) -> ExecuteTransactionResponse {
160 let parsed: Vec<partiql::parser::Statement> = statements
161 .iter()
162 .filter_map(|s| partiql::parser::parse(&s.statement).ok())
163 .collect();
164 ExecuteTransactionResponse {
165 responses: cached_responses,
166 consumed_capacity: crate::types::build_transactional_capacity(
167 &statement_table_units(parsed.iter()),
168 mode,
169 crate::types::transactional_read_capacity,
170 ),
171 }
172}
173
174async fn execute_within_transaction<S: StorageBackend>(
175 storage: &S,
176 parsed: &[(partiql::parser::Statement, Vec<AttributeValue>)],
177) -> Result<Vec<ItemResponse>> {
178 let mut responses = Vec::with_capacity(parsed.len());
179 let mut cancellation_reasons: Vec<CancellationReason> = Vec::with_capacity(parsed.len());
180
181 for (stmt, params) in parsed {
182 match partiql::executor::execute(storage, stmt, params, None).await {
183 Ok(result) => {
184 let item = result.and_then(|items| items.into_iter().next());
185 responses.push(ItemResponse { item });
186 cancellation_reasons.push(CancellationReason {
187 code: "None".to_string(),
188 message: None,
189 item: None,
190 });
191 }
192 Err(e) => {
193 let message = Some(e.to_string());
195 let (code, item) = match e {
196 DynoxideError::ConditionalCheckFailedException(_, item) => {
197 ("ConditionalCheckFailed".to_string(), item)
198 }
199 DynoxideError::DuplicateItemException(_) => ("DuplicateItem".to_string(), None),
200 DynoxideError::ValidationException(_)
204 | DynoxideError::KeyEmptyValueValidation(_) => {
205 ("ValidationError".to_string(), None)
206 }
207 _ => ("InternalError".to_string(), None),
208 };
209 responses.push(ItemResponse { item: None });
210 cancellation_reasons.push(CancellationReason {
211 code,
212 message,
213 item,
214 });
215
216 for _ in responses.len()..parsed.len() {
219 responses.push(ItemResponse { item: None });
220 cancellation_reasons.push(CancellationReason {
221 code: "None".to_string(),
222 message: None,
223 item: None,
224 });
225 }
226
227 let codes: Vec<&str> = cancellation_reasons
228 .iter()
229 .map(|r| r.code.as_str())
230 .collect();
231 let message = format!(
232 "Transaction cancelled, please refer cancellation reasons for specific reasons [{}]",
233 codes.join(", ")
234 );
235 return Err(DynoxideError::TransactionCanceledException(
236 message,
237 cancellation_reasons,
238 ));
239 }
240 }
241 }
242
243 Ok(responses)
244}