Skip to main content

dynoxide/actions/
batch_execute_statement.rs

1use crate::errors::{DynoxideError, Result};
2use crate::partiql;
3use crate::storage_backend::StorageBackend;
4use crate::types::{AttributeValue, Item};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Default, Deserialize)]
8pub struct BatchExecuteStatementRequest {
9    #[serde(rename = "Statements")]
10    pub statements: Vec<BatchStatementRequest>,
11}
12
13#[derive(Debug, Default, Deserialize)]
14pub struct BatchStatementRequest {
15    #[serde(rename = "Statement")]
16    pub statement: String,
17    #[serde(rename = "Parameters", default)]
18    pub parameters: Option<Vec<AttributeValue>>,
19}
20
21#[derive(Debug, Default, Serialize)]
22pub struct BatchExecuteStatementResponse {
23    #[serde(rename = "Responses")]
24    pub responses: Vec<BatchStatementResponse>,
25}
26
27#[derive(Debug, Default, Serialize)]
28#[non_exhaustive]
29pub struct BatchStatementResponse {
30    #[serde(rename = "Error", skip_serializing_if = "Option::is_none")]
31    pub error: Option<BatchStatementError>,
32    #[serde(rename = "Item", skip_serializing_if = "Option::is_none")]
33    pub item: Option<Item>,
34    /// The member statement's target table, echoed on a successful response
35    /// (with or without an `Item`), matching DynamoDB. Omitted on a
36    /// per-statement error and when the statement fails to parse.
37    #[serde(rename = "TableName", skip_serializing_if = "Option::is_none")]
38    pub table_name: Option<String>,
39}
40
41#[derive(Debug, Default, Serialize)]
42pub struct BatchStatementError {
43    #[serde(rename = "Code")]
44    pub code: String,
45    #[serde(rename = "Message")]
46    pub message: String,
47}
48
49pub async fn execute<S: StorageBackend>(
50    storage: &S,
51    request: BatchExecuteStatementRequest,
52) -> Result<BatchExecuteStatementResponse> {
53    if request.statements.is_empty() {
54        return Err(DynoxideError::ValidationException(
55            "1 validation error detected: Value '[]' at 'statements' failed to satisfy constraint: Member must have length greater than or equal to 1".to_string(),
56        ));
57    }
58
59    if request.statements.len() > 25 {
60        return Err(DynoxideError::ValidationException(
61            "Too many statements in BatchExecuteStatement; limit is 25".to_string(),
62        ));
63    }
64
65    let mut responses = Vec::with_capacity(request.statements.len());
66
67    for stmt_req in &request.statements {
68        let parsed = partiql::parser::parse(&stmt_req.statement);
69
70        let response = match parsed {
71            Err(e) => BatchStatementResponse {
72                error: Some(BatchStatementError {
73                    // A per-statement parse failure carries the short-form
74                    // `ValidationError` code, the same as an execution error,
75                    // matching DynamoDB.
76                    code: "ValidationError".to_string(),
77                    message: format!("Statement wasn't well formed, can't be processed: {e}"),
78                }),
79                item: None,
80                table_name: None,
81            },
82            Ok(stmt) => {
83                // DynamoDB echoes the target table on a successful response, but
84                // not on a per-statement error.
85                let table = partiql::parser::table_name(&stmt).map(str::to_string);
86                let params = stmt_req.parameters.as_deref().unwrap_or_default();
87                match partiql::executor::execute(storage, &stmt, params, None).await {
88                    Ok(Some(items)) => {
89                        // A SELECT yields its row here; a DELETE/UPDATE carrying a
90                        // RETURNING clause yields the returned item. Batch surfaces
91                        // a single item per statement, so take the first.
92                        BatchStatementResponse {
93                            error: None,
94                            item: items.into_iter().next(),
95                            table_name: table,
96                        }
97                    }
98                    Ok(None) => BatchStatementResponse {
99                        error: None,
100                        item: None,
101                        table_name: table,
102                    },
103                    Err(e) => BatchStatementResponse {
104                        error: Some(BatchStatementError {
105                            code: e.short_error_code().to_string(),
106                            message: e.to_string(),
107                        }),
108                        item: None,
109                        table_name: None,
110                    },
111                }
112            }
113        };
114
115        responses.push(response);
116    }
117
118    Ok(BatchExecuteStatementResponse { responses })
119}