dynoxide/actions/
batch_get_item.rs1use crate::actions::helpers;
2use crate::errors::{DynoxideError, Result};
3use crate::expressions;
4use crate::storage_backend::StorageBackend;
5use crate::types::{AttributeValue, Item};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Default, Deserialize)]
10pub struct BatchGetItemRequest {
11 #[serde(rename = "RequestItems")]
12 pub request_items: HashMap<String, KeysAndAttributes>,
13 #[serde(rename = "ReturnConsumedCapacity", default)]
14 pub return_consumed_capacity: Option<String>,
15}
16
17#[derive(Debug, Default, Deserialize)]
18pub struct KeysAndAttributes {
19 #[serde(rename = "Keys")]
20 pub keys: Vec<HashMap<String, AttributeValue>>,
21 #[serde(rename = "ProjectionExpression", default)]
22 pub projection_expression: Option<String>,
23 #[serde(rename = "ExpressionAttributeNames", default)]
24 pub expression_attribute_names: Option<HashMap<String, String>>,
25 #[serde(rename = "ConsistentRead", default)]
26 pub consistent_read: Option<bool>,
27 #[serde(rename = "AttributesToGet", default)]
28 pub attributes_to_get: Option<Vec<String>>,
29}
30
31#[derive(Debug, Default, Serialize)]
32pub struct BatchGetItemResponse {
33 #[serde(rename = "Responses")]
34 pub responses: HashMap<String, Vec<Item>>,
35 #[serde(rename = "UnprocessedKeys")]
36 pub unprocessed_keys: HashMap<String, serde_json::Value>,
37 #[serde(rename = "ConsumedCapacity", skip_serializing_if = "Option::is_none")]
38 pub consumed_capacity: Option<Vec<crate::types::ConsumedCapacity>>,
39}
40
41pub async fn execute<S: StorageBackend>(
42 storage: &S,
43 request: BatchGetItemRequest,
44) -> Result<BatchGetItemResponse> {
45 if request.request_items.is_empty() {
49 return Err(DynoxideError::ValidationException(
50 "The requestItems parameter is required for BatchGetItem".to_string(),
51 ));
52 }
53
54 for (table_name, ka) in &request.request_items {
56 if ka.keys.is_empty() {
57 return Err(DynoxideError::ValidationException(format!(
58 "1 validation error detected: Value at 'requestItems.{table_name}.member.keys' failed to satisfy constraint: Member must have length greater than or equal to 1"
59 )));
60 }
61 }
62
63 for table_name in request.request_items.keys() {
65 crate::validation::validate_table_name(table_name)?;
66 }
67
68 let total_keys: usize = request.request_items.values().map(|ka| ka.keys.len()).sum();
76 if total_keys > 100 {
77 let table_name = request
78 .request_items
79 .iter()
80 .max_by_key(|(_, ka)| ka.keys.len())
81 .map(|(name, _)| name.as_str())
82 .unwrap_or("");
83 return Err(DynoxideError::ValidationException(format!(
84 "1 validation error detected: Value at 'RequestItems.{table_name}.member.Keys' failed to satisfy constraint: Member must have length less than or equal to 100"
85 )));
86 }
87
88 let any_projection = request
97 .request_items
98 .values()
99 .any(|ka| ka.projection_expression.is_some());
100 let any_attributes_to_get = request
101 .request_items
102 .values()
103 .any(|ka| ka.attributes_to_get.is_some());
104 if any_projection && any_attributes_to_get {
105 return Err(DynoxideError::ValidationException(
106 "Can not use both expression and non-expression parameters in the same request: Non-expression parameters: {AttributesToGet} Expression parameters: {ProjectionExpression}".to_string(),
107 ));
108 }
109
110 for keys_and_attrs in request.request_items.values() {
111 let has_projection_expr = keys_and_attrs.projection_expression.is_some();
112 let has_expr_attr_names = keys_and_attrs.expression_attribute_names.is_some();
113
114 if has_expr_attr_names && !has_projection_expr {
116 return Err(DynoxideError::ValidationException(
117 "ExpressionAttributeNames can only be specified when using expressions".to_string(),
118 ));
119 }
120
121 if let Some(ref ean) = keys_and_attrs.expression_attribute_names {
123 if ean.is_empty() {
124 return Err(DynoxideError::ValidationException(
125 "ExpressionAttributeNames must not be empty".to_string(),
126 ));
127 }
128 for key in ean.keys() {
130 if !key.starts_with('#') {
131 return Err(DynoxideError::ValidationException(format!(
132 "ExpressionAttributeNames contains invalid key: Syntax error; key: \"{key}\""
133 )));
134 }
135 }
136 }
137
138 if let Some(ref pe) = keys_and_attrs.projection_expression {
140 if pe.is_empty() {
141 return Err(DynoxideError::ValidationException(
142 "Invalid ProjectionExpression: The expression can not be empty;".to_string(),
143 ));
144 }
145 }
146
147 if let Some(ref atg) = keys_and_attrs.attributes_to_get {
149 let mut seen = std::collections::HashSet::new();
150 for attr in atg {
151 if !seen.insert(attr.as_str()) {
152 return Err(DynoxideError::ValidationException(format!(
153 "One or more parameter values were invalid: Duplicate value in attribute name: {attr}"
154 )));
155 }
156 }
157 }
158
159 for key in &keys_and_attrs.keys {
161 crate::validation::validate_item_attribute_values(key)?;
162 }
163
164 if keys_and_attrs.keys.len() > 1 {
166 let serialised: Vec<String> = keys_and_attrs
167 .keys
168 .iter()
169 .map(|k| {
170 let mut pairs: Vec<_> = k.iter().map(|(k, v)| format!("{k}={v:?}")).collect();
171 pairs.sort();
172 pairs.join(",")
173 })
174 .collect();
175 let mut seen = std::collections::HashSet::new();
176 for s in &serialised {
177 if !seen.insert(s) {
178 return Err(DynoxideError::ValidationException(
179 "Provided list of item keys contains duplicates".to_string(),
180 ));
181 }
182 }
183 }
184 }
185
186 const MAX_RESPONSE_SIZE: usize = 16 * 1024 * 1024; let mut responses: HashMap<String, Vec<Item>> = HashMap::new();
189 let mut unprocessed_keys: HashMap<String, serde_json::Value> = HashMap::new();
190 let mut cumulative_size: usize = 0;
191 let mut size_limit_reached = false;
192 let mut table_rcu: HashMap<String, f64> = HashMap::new();
194
195 for (table_name, keys_and_attrs) in &request.request_items {
196 let meta = helpers::require_table_for_item_op(storage, table_name).await?;
197 let key_schema = helpers::parse_key_schema(&meta)?;
198
199 let projection = if let Some(ref expr) = keys_and_attrs.projection_expression {
201 Some(expressions::projection::parse(expr).map_err(DynoxideError::ValidationException)?)
202 } else {
203 keys_and_attrs
204 .attributes_to_get
205 .as_ref()
206 .map(|attrs| crate::actions::helpers::attributes_to_get_to_projection(attrs))
207 };
208
209 let tracker = crate::expressions::TrackedExpressionAttributes::new(
210 &keys_and_attrs.expression_attribute_names,
211 &None, );
213
214 if let Some(ref proj) = projection {
216 tracker.track_projection_expr(proj);
217 expressions::projection::validate(proj, &tracker)
219 .map_err(DynoxideError::ValidationException)?;
220 }
221
222 let key_attrs = Vec::new();
224
225 let consistent = keys_and_attrs.consistent_read.unwrap_or(false);
226 let mut table_items = Vec::new();
227 let mut remaining_keys: Vec<HashMap<String, AttributeValue>> = Vec::new();
228 let mut per_table_rcu: f64 = 0.0;
229
230 for key in &keys_and_attrs.keys {
231 if size_limit_reached {
232 remaining_keys.push(key.clone());
233 continue;
234 }
235
236 helpers::validate_key_only(key, &key_schema)?;
237 let (pk, sk) = helpers::extract_key_strings(key, &key_schema)?;
239
240 if let Some(item_json) = storage.get_item(table_name, &pk, &sk).await? {
241 let item: Item = serde_json::from_str(&item_json).map_err(|e| {
242 DynoxideError::InternalServerError(format!("Bad item JSON: {e}"))
243 })?;
244
245 let item_size = crate::types::item_size(&item);
247
248 if cumulative_size + item_size > MAX_RESPONSE_SIZE {
249 size_limit_reached = true;
250 remaining_keys.push(key.clone());
251 continue;
252 }
253
254 cumulative_size += item_size;
255
256 per_table_rcu +=
258 crate::types::read_capacity_units_with_consistency(item_size, consistent);
259
260 let result_item = if let Some(ref proj) = projection {
261 expressions::projection::apply(&item, proj, &tracker, &key_attrs)
262 .map_err(DynoxideError::ValidationException)?
263 } else {
264 item
265 };
266
267 table_items.push(result_item);
268 } else {
269 per_table_rcu += crate::types::read_capacity_units_with_consistency(0, consistent);
271 }
272 }
273
274 tracker.check_unused()?;
276
277 table_rcu.insert(table_name.clone(), per_table_rcu);
278 responses.insert(table_name.clone(), table_items);
279
280 if !remaining_keys.is_empty() {
281 let mut unprocessed = serde_json::json!({
282 "Keys": remaining_keys,
283 });
284 if let Some(ref pe) = keys_and_attrs.projection_expression {
287 unprocessed["ProjectionExpression"] = serde_json::json!(pe);
288 }
289 if let Some(ref ean) = keys_and_attrs.expression_attribute_names {
290 unprocessed["ExpressionAttributeNames"] = serde_json::json!(ean);
291 }
292 if let Some(cr) = keys_and_attrs.consistent_read {
293 unprocessed["ConsistentRead"] = serde_json::json!(cr);
294 }
295 unprocessed_keys.insert(table_name.clone(), unprocessed);
296 }
297 }
298
299 let consumed_capacity = if matches!(
301 request.return_consumed_capacity.as_deref(),
302 Some("TOTAL") | Some("INDEXES")
303 ) {
304 let mut caps = Vec::new();
305 for table_name in request.request_items.keys() {
306 let total_rcu = table_rcu.get(table_name).copied().unwrap_or(0.0);
307 if let Some(cc) = crate::types::consumed_capacity(
308 table_name,
309 total_rcu,
310 &request.return_consumed_capacity,
311 ) {
312 caps.push(cc);
313 }
314 }
315 Some(caps)
316 } else {
317 None
318 };
319
320 Ok(BatchGetItemResponse {
321 responses,
322 unprocessed_keys,
323 consumed_capacity,
324 })
325}