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 ExecuteStatementRequest {
9 #[serde(rename = "Statement")]
10 pub statement: String,
11 #[serde(
12 rename = "Parameters",
13 default,
14 deserialize_with = "crate::types::deserialize_parameters"
15 )]
16 pub parameters: Option<Vec<AttributeValue>>,
17 #[serde(rename = "Limit", default)]
18 pub limit: Option<usize>,
19 #[serde(rename = "NextToken", default)]
20 pub next_token: Option<String>,
21 #[serde(rename = "ConsistentRead", default)]
26 pub consistent_read: Option<bool>,
27 #[serde(rename = "ReturnConsumedCapacity", default)]
28 pub return_consumed_capacity: Option<String>,
29 #[serde(rename = "ReturnValuesOnConditionCheckFailure", default)]
33 pub return_values_on_condition_check_failure: Option<String>,
34}
35
36#[derive(Debug, Default, Serialize)]
37pub struct ExecuteStatementResponse {
38 #[serde(
39 rename = "Items",
40 skip_serializing_if = "Option::is_none",
41 serialize_with = "crate::set_order::items_in_hash_order"
42 )]
43 pub items: Option<Vec<Item>>,
46 #[serde(rename = "NextToken", skip_serializing_if = "Option::is_none")]
47 pub next_token: Option<String>,
48 #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
49 pub consumed_capacity: Option<crate::types::ConsumedCapacity>,
50}
51
52pub async fn execute<S: StorageBackend>(
53 storage: &S,
54 request: ExecuteStatementRequest,
55) -> Result<ExecuteStatementResponse> {
56 let violations: Vec<String> = [
65 crate::validation::return_values_on_condition_check_failure_rejection(
66 request.return_values_on_condition_check_failure.as_deref(),
67 "returnValuesOnConditionCheckFailure",
68 ),
69 (request.limit == Some(0)).then(|| {
70 "Value '0' at 'limit' failed to satisfy constraint: Member must have value \
71 greater than or equal to 1"
72 .to_string()
73 }),
74 crate::partiql::statement_length_violation(&request.statement, "statement"),
75 crate::validation::return_consumed_capacity_rejection(
76 request.return_consumed_capacity.as_deref(),
77 ),
78 crate::partiql::parameters_violation(request.parameters.as_deref(), "parameters"),
79 ]
80 .into_iter()
81 .flatten()
82 .collect();
83 if let Some(e) = crate::partiql::member_rejection(&violations) {
84 return Err(e);
85 }
86
87 if crate::empty_names::parameters_have_empty_name(request.parameters.as_deref()) {
90 return Err(crate::empty_names::bare());
91 }
92
93 if crate::value_nesting::parameters_nest_too_deep(
96 request.parameters.as_deref().unwrap_or_default(),
97 ) {
98 return Err(crate::value_nesting::deep_parameter_error());
99 }
100
101 for parameter in request.parameters.iter().flatten() {
106 partiql::executor::check_parameter_value(parameter)?;
107 }
108
109 if let Some(msg) = partiql::parser::count_projection_rejection(&request.statement) {
115 if let Some((table, Some(index))) = partiql::parser::select_target(&request.statement) {
116 partiql::executor::reject_multi_attribute_index_by_name(storage, &table, &index)
117 .await?;
118 }
119 return Err(DynoxideError::ValidationException(msg));
120 }
121
122 let supplied = request.parameters.as_ref().map_or(0, Vec::len);
126 let stmt = partiql::parser::parse_with_parameters(&request.statement, supplied).map_err(
127 |failure| {
128 DynoxideError::ValidationException(match failure {
129 partiql::parser::ParseFailure::Statement(e) => e.into_message(),
130 partiql::parser::ParseFailure::Literal(message) => message,
131 })
132 },
133 )?;
134
135 let params = request.parameters.unwrap_or_default();
136 let run = partiql::executor::execute_written_page(
139 storage,
140 &request.statement,
141 &stmt,
142 ¶ms,
143 request.limit,
144 request.next_token.as_deref(),
145 request.consistent_read.unwrap_or(false),
146 request.return_consumed_capacity.as_deref(),
147 None,
150 request.return_values_on_condition_check_failure.as_deref() == Some("ALL_OLD"),
151 );
152 let page = if matches!(stmt, partiql::parser::Statement::Select { .. }) {
157 run.await?
158 } else {
159 super::helpers::with_write_transaction(storage, run).await?
160 };
161 let partiql::executor::StatementPage {
162 items,
163 size,
164 capacity,
165 next_token,
166 read_index,
167 base_read_units,
168 } = page;
169
170 let consumed_capacity = partiql::parser::table_name(&stmt).and_then(|table| {
179 let Some(capacity) = capacity else {
180 let units = crate::types::read_capacity_units_with_consistency(
181 size,
182 request.consistent_read.unwrap_or(false),
183 );
184 return match read_index {
189 Some(index) if index.is_lsi => {
190 let lsi_units = std::collections::HashMap::from([(index.name, units)]);
200 crate::types::consumed_capacity_with_secondary_indexes(
201 table,
202 base_read_units,
203 &std::collections::HashMap::new(),
204 &lsi_units,
205 &request.return_consumed_capacity,
206 )
207 }
208 Some(index) => {
209 let gsi_units = std::collections::HashMap::from([(index.name, units)]);
210 crate::types::consumed_capacity_with_indexes(
211 table,
212 0.0,
213 &gsi_units,
214 &request.return_consumed_capacity,
215 )
216 }
217 None => {
218 crate::types::consumed_capacity(table, units, &request.return_consumed_capacity)
219 }
220 };
221 };
222 crate::types::consumed_capacity_with_vector_indexes(
223 table,
224 crate::types::table_write_capacity_units(capacity.old_size, capacity.new_size),
225 &capacity.gsi_units,
226 &capacity.lsi_units,
227 &capacity.vector_bytes,
228 &request.return_consumed_capacity,
229 )
230 });
231
232 Ok(ExecuteStatementResponse {
236 items: Some(items.unwrap_or_default()),
237 next_token,
238 consumed_capacity,
239 })
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::storage::Storage;
246
247 async fn table_with_two_gsis(storage: &Storage) {
249 let req = serde_json::from_value(serde_json::json!({
250 "TableName": "pq_tx",
251 "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
252 "AttributeDefinitions": [
253 {"AttributeName": "pk", "AttributeType": "S"},
254 {"AttributeName": "g1", "AttributeType": "S"},
255 {"AttributeName": "g2", "AttributeType": "S"}
256 ],
257 "BillingMode": "PAY_PER_REQUEST",
258 "GlobalSecondaryIndexes": [
259 {
260 "IndexName": "gsi-one",
261 "KeySchema": [{"AttributeName": "g1", "KeyType": "HASH"}],
262 "Projection": {"ProjectionType": "ALL"}
263 },
264 {
265 "IndexName": "gsi-two",
266 "KeySchema": [{"AttributeName": "g2", "KeyType": "HASH"}],
267 "Projection": {"ProjectionType": "ALL"}
268 }
269 ]
270 }))
271 .unwrap();
272 crate::actions::create_table::execute(storage, req)
273 .await
274 .unwrap();
275 }
276
277 fn s(v: &str) -> AttributeValue {
278 AttributeValue::S(v.to_string())
279 }
280
281 async fn val_of(storage: &Storage, pk: &str) -> String {
282 let req = crate::actions::get_item::GetItemRequest {
283 table_name: "pq_tx".to_string(),
284 key: [("pk".to_string(), s(pk))].into_iter().collect(),
285 ..Default::default()
286 };
287 let got = crate::actions::get_item::execute(storage, req)
288 .await
289 .unwrap();
290 match got.item.unwrap().get("val").unwrap() {
291 AttributeValue::S(v) => v.clone(),
292 other => panic!("unexpected val: {other:?}"),
293 }
294 }
295
296 #[tokio::test]
297 async fn a_failed_fan_out_rolls_the_base_row_back() {
298 let storage = Storage::memory().unwrap();
303 table_with_two_gsis(&storage).await;
304
305 let put = crate::actions::put_item::PutItemRequest {
306 table_name: "pq_tx".to_string(),
307 item: [
308 ("pk".to_string(), s("a")),
309 ("g1".to_string(), s("x")),
310 ("g2".to_string(), s("y")),
311 ("val".to_string(), s("before")),
312 ]
313 .into_iter()
314 .collect(),
315 ..Default::default()
316 };
317 crate::actions::put_item::execute(&storage, put)
318 .await
319 .unwrap();
320 assert_eq!(val_of(&storage, "a").await, "before");
321
322 storage.fail_gsi_insert_after(1);
324 let err = execute(
325 &storage,
326 ExecuteStatementRequest {
327 statement: "UPDATE \"pq_tx\" SET val='after' WHERE pk='a'".to_string(),
328 ..Default::default()
329 },
330 )
331 .await
332 .expect_err("the injected failure must surface");
333 assert!(
334 format!("{err:?}").contains("injected GSI insert failure"),
335 "unexpected error: {err:?}"
336 );
337
338 assert_eq!(
339 val_of(&storage, "a").await,
340 "before",
341 "the base row must roll back with the fan-out that failed"
342 );
343 }
344}