1use crate::actions::create_table::StreamSpecification;
2use crate::actions::{TableDescription, build_table_description};
3use crate::actions::{gsi, helpers};
4use crate::errors::{DynoxideError, Result};
5use crate::storage_backend::StorageBackend;
6use crate::streams;
7use crate::types::{AttributeDefinition, GlobalSecondaryIndex, KeySchemaElement, Projection};
8use crate::validation;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Default, Deserialize)]
13struct UpdateTableRequestRaw {
14 #[serde(rename = "TableName", default)]
15 table_name: Option<String>,
16
17 #[serde(rename = "AttributeDefinitions", default)]
18 attribute_definitions: Option<Vec<AttributeDefinition>>,
19
20 #[serde(rename = "GlobalSecondaryIndexUpdates", default)]
21 global_secondary_index_updates: Option<Vec<GlobalSecondaryIndexUpdate>>,
22
23 #[serde(rename = "StreamSpecification", default)]
24 stream_specification: Option<StreamSpecification>,
25
26 #[serde(rename = "DeletionProtectionEnabled", default)]
27 deletion_protection_enabled: Option<bool>,
28
29 #[serde(rename = "ProvisionedThroughput", default)]
30 provisioned_throughput: Option<serde_json::Value>,
31
32 #[serde(rename = "BillingMode", default)]
33 billing_mode: Option<String>,
34
35 #[serde(rename = "TableClass", default)]
36 table_class: Option<String>,
37
38 #[serde(rename = "OnDemandThroughput", default)]
39 on_demand_throughput: Option<crate::types::OnDemandThroughput>,
40}
41
42#[derive(Debug, Default)]
43pub struct UpdateTableRequest {
44 pub table_name: String,
45 pub attribute_definitions: Option<Vec<AttributeDefinition>>,
46 pub global_secondary_index_updates: Option<Vec<GlobalSecondaryIndexUpdate>>,
47 pub stream_specification: Option<StreamSpecification>,
48 pub deletion_protection_enabled: Option<bool>,
49 pub provisioned_throughput: Option<serde_json::Value>,
50 pub billing_mode: Option<String>,
51 pub table_class: Option<String>,
52 pub on_demand_throughput: Option<crate::types::OnDemandThroughput>,
53}
54
55impl<'de> serde::Deserialize<'de> for UpdateTableRequest {
56 fn deserialize<D: serde::Deserializer<'de>>(
57 deserializer: D,
58 ) -> std::result::Result<Self, D::Error> {
59 let raw = UpdateTableRequestRaw::deserialize(deserializer)?;
60
61 if raw.table_name.is_none() || raw.table_name.as_deref() == Some("") {
63 let msg = if raw.table_name.is_none() {
64 "The parameter 'TableName' is required but was not present in the request"
65 } else {
66 "TableName must be at least 3 characters long and at most 255 characters long"
67 };
68 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
69 }
70
71 let table_name = raw.table_name.unwrap_or_default();
72
73 if table_name.len() < 3 || table_name.len() > 255 {
75 return Err(serde::de::Error::custom(
76 "VALIDATION:TableName must be at least 3 characters long and at most 255 characters long",
77 ));
78 }
79
80 let mut errors = Vec::new();
82
83 if !table_name
84 .chars()
85 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
86 {
87 errors.push(format!(
88 "Value '{}' at 'tableName' failed to satisfy constraint: \
89 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
90 table_name
91 ));
92 }
93
94 if let Some(msg) = crate::validation::format_validation_errors(&errors) {
95 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
96 }
97
98 Ok(UpdateTableRequest {
99 table_name,
100 attribute_definitions: raw.attribute_definitions,
101 global_secondary_index_updates: raw.global_secondary_index_updates,
102 stream_specification: raw.stream_specification,
103 deletion_protection_enabled: raw.deletion_protection_enabled,
104 provisioned_throughput: raw.provisioned_throughput,
105 billing_mode: raw.billing_mode,
106 table_class: raw.table_class,
107 on_demand_throughput: raw.on_demand_throughput,
108 })
109 }
110}
111
112#[derive(Debug, Default, Deserialize)]
113pub struct GlobalSecondaryIndexUpdate {
114 #[serde(rename = "Update", default)]
115 pub update: Option<UpdateGsiAction>,
116
117 #[serde(rename = "Create", default)]
118 pub create: Option<CreateGsiAction>,
119
120 #[serde(rename = "Delete", default)]
121 pub delete: Option<DeleteGsiAction>,
122}
123
124#[derive(Debug, Default, Deserialize)]
125pub struct UpdateGsiAction {
126 #[serde(rename = "IndexName")]
127 pub index_name: String,
128
129 #[serde(rename = "ProvisionedThroughput", default)]
130 pub provisioned_throughput: Option<crate::types::ProvisionedThroughput>,
131}
132
133#[derive(Debug, Default, Deserialize)]
134pub struct CreateGsiAction {
135 #[serde(rename = "IndexName")]
136 pub index_name: String,
137
138 #[serde(rename = "KeySchema")]
139 pub key_schema: Vec<KeySchemaElement>,
140
141 #[serde(rename = "Projection")]
142 pub projection: Projection,
143}
144
145#[derive(Debug, Default, Deserialize)]
146pub struct DeleteGsiAction {
147 #[serde(rename = "IndexName")]
148 pub index_name: String,
149}
150
151#[derive(Debug, Default, Serialize)]
152pub struct UpdateTableResponse {
153 #[serde(rename = "TableDescription")]
154 pub table_description: TableDescription,
155}
156
157pub async fn execute<S: StorageBackend>(
158 storage: &S,
159 mut request: UpdateTableRequest,
160) -> Result<UpdateTableResponse> {
161 if request.on_demand_throughput.as_ref().is_some_and(|odt| {
169 odt.max_read_request_units.is_none() && odt.max_write_request_units.is_none()
170 }) {
171 request.on_demand_throughput = None;
172 }
173
174 validate_update_request(&request)?;
178
179 let meta = helpers::require_table(storage, &request.table_name).await?;
181
182 let current_billing_mode = meta.billing_mode.as_deref().unwrap_or("PROVISIONED");
183
184 if current_billing_mode == "PAY_PER_REQUEST"
188 && request.billing_mode.is_none()
189 && request.provisioned_throughput.is_some()
190 {
191 return Err(DynoxideError::ValidationException(
192 "One or more parameter values were invalid: \
193 Neither ReadCapacityUnits nor WriteCapacityUnits can be \
194 specified when BillingMode is PAY_PER_REQUEST"
195 .to_string(),
196 ));
197 }
198
199 if request.billing_mode.as_deref() == Some("PROVISIONED")
201 && request.provisioned_throughput.is_none()
202 {
203 return Err(DynoxideError::ValidationException(
204 "One or more parameter values were invalid: \
205 ProvisionedThroughput must be specified when BillingMode is PROVISIONED"
206 .to_string(),
207 ));
208 }
209
210 if let Some(ref odt) = request.on_demand_throughput {
218 let target_is_provisioned = match request.billing_mode.as_deref() {
219 Some("PAY_PER_REQUEST") => false,
220 Some(_) => true,
221 None => current_billing_mode == "PROVISIONED",
222 };
223 let members = [
224 ("MaxReadRequestUnits", odt.max_read_request_units),
225 ("MaxWriteRequestUnits", odt.max_write_request_units),
226 ];
227 if target_is_provisioned {
228 if let Some((member, _)) = members.iter().find(|(_, v)| v.is_some()) {
229 return Err(DynoxideError::ValidationException(format!(
230 "One or more parameter values were invalid: {member} for \
231 OnDemandThroughput cannot be specified when the table BillingMode \
232 is PROVISIONED"
233 )));
234 }
235 }
236 for (member, value) in members {
239 if value.is_some_and(|v| v == 0 || v < -1) {
240 return Err(DynoxideError::ValidationException(format!(
241 "One or more parameter values were invalid: Requested {member} for \
242 OnDemandThroughput for table is outside of valid range"
243 )));
244 }
245 }
246 }
247
248 if let Some(ref pt) = request.provisioned_throughput {
250 if let Some(obj) = pt.as_object() {
251 let new_rcu = obj
252 .get("ReadCapacityUnits")
253 .and_then(|v| v.as_i64())
254 .unwrap_or(0);
255 let new_wcu = obj
256 .get("WriteCapacityUnits")
257 .and_then(|v| v.as_i64())
258 .unwrap_or(0);
259
260 let (cur_rcu, cur_wcu) = parse_current_throughput(&meta);
262
263 let billing_mode_unchanged = request.billing_mode.is_none()
264 || (request.billing_mode.as_deref() == Some("PROVISIONED")
265 && current_billing_mode == "PROVISIONED");
266
267 if new_rcu == cur_rcu && new_wcu == cur_wcu && billing_mode_unchanged {
268 return Err(DynoxideError::ValidationException(format!(
269 "The provisioned throughput for the table will not change. \
270 The requested value equals the current value. \
271 Current ReadCapacityUnits provisioned for the table: {}. \
272 Requested ReadCapacityUnits: {}. \
273 Current WriteCapacityUnits provisioned for the table: {}. \
274 Requested WriteCapacityUnits: {}. \
275 Refer to the Amazon DynamoDB Developer Guide for current limits \
276 and how to request higher limits.",
277 cur_rcu, new_rcu, cur_wcu, new_wcu
278 )));
279 }
280 }
281 }
282
283 if request.stream_specification.is_some() && !storage.supports_streams() {
288 return Err(crate::storage_backend::BackendError::Unsupported {
289 capability: "streams",
290 }
291 .into());
292 }
293
294 let mut current_gsis: Vec<GlobalSecondaryIndex> = meta
296 .gsi_definitions
297 .as_ref()
298 .map(|json| serde_json::from_str(json))
299 .transpose()
300 .map_err(|e| DynoxideError::InternalServerError(format!("Bad GSI JSON: {e}")))?
301 .unwrap_or_default();
302
303 if let Some(ref updates) = request.global_secondary_index_updates {
305 for update in updates {
306 if let Some(ref upd) = update.update {
307 if !current_gsis.iter().any(|g| g.index_name == upd.index_name) {
308 return Err(DynoxideError::ValidationException(
311 "This operation cannot be performed with given input values. \
312 Please contact DynamoDB service team for more info: \
313 Action Blocked: IndexUpdate"
314 .to_string(),
315 ));
316 }
317 }
318 }
319 }
320
321 if let Some(ref updates) = request.global_secondary_index_updates {
323 if updates.len() > 5 {
324 return Err(DynoxideError::LimitExceededException(
325 "Subscriber limit exceeded: Only 1 online index can be created or \
326 deleted simultaneously per table"
327 .to_string(),
328 ));
329 }
330 }
331
332 let mut attr_defs: Vec<AttributeDefinition> = serde_json::from_str(&meta.attribute_definitions)
338 .map_err(|e| DynoxideError::InternalServerError(format!("Bad attr defs JSON: {e}")))?;
339
340 if let Some(ref provided) = request.attribute_definitions {
341 for def in provided {
342 if !attr_defs
347 .iter()
348 .any(|d| d.attribute_name == def.attribute_name)
349 {
350 attr_defs.push(def.clone());
351 }
352 }
353 }
354
355 let key_schema = helpers::parse_key_schema(&meta)?;
357
358 if let Some(ref updates) = request.global_secondary_index_updates {
360 for update in updates {
361 if let Some(ref create) = update.create {
362 if current_gsis
363 .iter()
364 .any(|g| g.index_name == create.index_name)
365 {
366 return Err(DynoxideError::ValidationException(format!(
367 "One or more parameter values were invalid: \
368 Index already exists: {}",
369 create.index_name
370 )));
371 }
372 let gsi_def = GlobalSecondaryIndex {
373 index_name: create.index_name.clone(),
374 key_schema: create.key_schema.clone(),
375 projection: create.projection.clone(),
376 provisioned_throughput: None,
377 };
378 validation::validate_gsi(
383 &gsi_def,
384 request.attribute_definitions.as_deref().unwrap_or(&[]),
385 )?;
386 }
387 if let Some(ref delete) = update.delete {
388 if !current_gsis
389 .iter()
390 .any(|g| g.index_name == delete.index_name)
391 {
392 return Err(DynoxideError::ResourceNotFoundException(format!(
393 "Requested resource not found: Table: {} not found",
394 delete.index_name
395 )));
396 }
397 }
398 }
399 }
400
401 let now = {
405 use std::sync::atomic::{AtomicU64, Ordering};
406 static LAST_TS: AtomicU64 = AtomicU64::new(0);
407 let wall = web_time::SystemTime::now()
408 .duration_since(web_time::UNIX_EPOCH)
409 .unwrap_or_default()
410 .as_secs_f64();
411 loop {
412 let prev_bits = LAST_TS.load(Ordering::SeqCst);
413 let prev_f = f64::from_bits(prev_bits);
414 let candidate = if wall > prev_f { wall } else { prev_f + 0.001 };
415 let candidate_bits = candidate.to_bits();
416 if LAST_TS
417 .compare_exchange(
418 prev_bits,
419 candidate_bits,
420 Ordering::SeqCst,
421 Ordering::SeqCst,
422 )
423 .is_ok()
424 {
425 break candidate;
426 }
427 }
428 };
429
430 let (cur_rcu, cur_wcu) = parse_current_throughput(&meta);
431 let is_pt_update = request.provisioned_throughput.is_some();
432 let (new_rcu, new_wcu) = if let Some(ref pt) = request.provisioned_throughput {
433 let obj = pt.as_object();
434 (
435 obj.and_then(|o| o.get("ReadCapacityUnits"))
436 .and_then(|v| v.as_i64())
437 .unwrap_or(0),
438 obj.and_then(|o| o.get("WriteCapacityUnits"))
439 .and_then(|v| v.as_i64())
440 .unwrap_or(0),
441 )
442 } else {
443 (cur_rcu, cur_wcu)
444 };
445
446 let is_increase = new_rcu > cur_rcu || new_wcu > cur_wcu;
447 let is_decrease = new_rcu < cur_rcu || new_wcu < cur_wcu;
448
449 let odt_change = request.on_demand_throughput.as_ref().map(|req_odt| {
454 let stored: crate::types::OnDemandThroughput = meta
455 .on_demand_throughput
456 .as_deref()
457 .and_then(|json| serde_json::from_str(json).ok())
458 .unwrap_or_default();
459 let echo = crate::types::OnDemandThroughput {
460 max_read_request_units: req_odt
461 .max_read_request_units
462 .or(stored.max_read_request_units),
463 max_write_request_units: req_odt
464 .max_write_request_units
465 .or(stored.max_write_request_units),
466 };
467 let strip = |v: Option<i64>| v.filter(|&v| v != -1);
468 let effective = crate::types::OnDemandThroughput {
469 max_read_request_units: strip(echo.max_read_request_units),
470 max_write_request_units: strip(echo.max_write_request_units),
471 };
472 (echo, effective)
473 });
474
475 helpers::with_write_transaction(storage, async {
477 if let Some(ref updates) = request.global_secondary_index_updates {
478 for update in updates {
479 if let Some(ref create) = update.create {
480 let gsi_def = GlobalSecondaryIndex {
481 index_name: create.index_name.clone(),
482 key_schema: create.key_schema.clone(),
483 projection: create.projection.clone(),
484 provisioned_throughput: None,
485 };
486
487 storage
488 .create_gsi_table(&request.table_name, &create.index_name)
489 .await?;
490
491 let gsi_p = gsi::gsi_to_def(&gsi_def)?;
492 backfill_gsi(storage, &request.table_name, &key_schema, &gsi_p).await?;
493
494 current_gsis.push(gsi_def);
495 }
496
497 if let Some(ref delete) = update.delete {
498 storage
499 .drop_gsi_table(&request.table_name, &delete.index_name)
500 .await?;
501 current_gsis.retain(|g| g.index_name != delete.index_name);
502 }
503 }
504 }
505
506 let lsi_defs = crate::actions::lsi::parse_lsi_defs(&meta)?;
510 reconcile_attribute_definitions(&mut attr_defs, &key_schema, ¤t_gsis, &lsi_defs);
511
512 let attr_defs_json = serde_json::to_string(&attr_defs)
514 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
515 let gsi_json = if current_gsis.is_empty() {
516 None
517 } else {
518 Some(
519 serde_json::to_string(¤t_gsis)
520 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?,
521 )
522 };
523
524 storage
525 .update_table_metadata(&request.table_name, &attr_defs_json, gsi_json.as_deref())
526 .await?;
527
528 if is_pt_update {
530 let prev = parse_stored_throughput(&meta);
531 let mut stored = StoredProvisionedThroughput {
532 read_capacity_units: new_rcu,
533 write_capacity_units: new_wcu,
534 last_increase_date_time: prev.as_ref().and_then(|p| p.last_increase_date_time),
535 last_decrease_date_time: prev.as_ref().and_then(|p| p.last_decrease_date_time),
536 number_of_decreases_today: prev
537 .as_ref()
538 .and_then(|p| p.number_of_decreases_today)
539 .or(Some(0)),
540 };
541 if is_increase {
542 stored.last_increase_date_time = Some(now);
543 }
544 if is_decrease {
545 stored.last_decrease_date_time = Some(now);
546 stored.number_of_decreases_today =
547 Some(stored.number_of_decreases_today.unwrap_or(0) + 1);
548 }
549 let pt_json = serde_json::to_string(&stored)
550 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
551 storage
552 .update_provisioned_throughput(&request.table_name, &pt_json)
553 .await?;
554 }
555
556 if let Some(enabled) = request.deletion_protection_enabled {
558 storage
559 .update_deletion_protection(&request.table_name, enabled)
560 .await?;
561 }
562
563 if let Some(ref table_class) = request.table_class {
565 storage
566 .update_table_class(&request.table_name, table_class)
567 .await?;
568 }
569
570 if let Some((_, ref effective)) = odt_change {
572 if effective.max_read_request_units.is_none()
573 && effective.max_write_request_units.is_none()
574 {
575 storage
576 .clear_on_demand_throughput(&request.table_name)
577 .await?;
578 } else {
579 let json = serde_json::to_string(effective)
580 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
581 storage
582 .update_on_demand_throughput(&request.table_name, &json)
583 .await?;
584 }
585 }
586
587 if let Some(ref billing_mode) = request.billing_mode {
589 storage
590 .update_billing_mode(&request.table_name, billing_mode)
591 .await?;
592 if billing_mode == "PAY_PER_REQUEST" {
593 storage
595 .clear_provisioned_throughput(&request.table_name)
596 .await?;
597 } else if billing_mode == "PROVISIONED" {
598 storage
601 .clear_on_demand_throughput(&request.table_name)
602 .await?;
603 }
604 }
605
606 if let Some(ref spec) = request.stream_specification {
608 if spec.stream_enabled {
609 let view_type = spec
610 .stream_view_type
611 .as_deref()
612 .unwrap_or("NEW_AND_OLD_IMAGES");
613 let label = streams::generate_stream_label(storage.clock());
614 storage
615 .enable_stream(&request.table_name, view_type, &label)
616 .await?;
617 } else {
618 storage.disable_stream(&request.table_name).await?;
619 }
620 }
621
622 Ok(())
623 })
624 .await?;
625
626 let updated_meta = helpers::require_table(storage, &request.table_name).await?;
628 let mut desc = build_table_description(&updated_meta, Some(0), Some(0));
629
630 if let Some((echo, _)) = odt_change {
633 desc.on_demand_throughput = Some(echo);
634 }
635
636 if is_pt_update {
638 desc.table_status = "UPDATING".to_string();
639
640 let stored = parse_stored_throughput(&updated_meta);
643 if let Some(ref mut pt) = desc.provisioned_throughput {
644 pt.read_capacity_units = cur_rcu as u64;
645 pt.write_capacity_units = cur_wcu as u64;
646 if let Some(ref s) = stored {
647 pt.last_increase_date_time = s.last_increase_date_time;
648 pt.last_decrease_date_time = s.last_decrease_date_time;
649 pt.number_of_decreases_today = s.number_of_decreases_today.unwrap_or(0);
650 }
651 }
652 }
653
654 Ok(UpdateTableResponse {
655 table_description: desc,
656 })
657}
658
659fn reconcile_attribute_definitions(
665 attr_defs: &mut Vec<AttributeDefinition>,
666 key_schema: &helpers::KeySchema,
667 gsis: &[GlobalSecondaryIndex],
668 lsi_defs: &[crate::actions::lsi::LsiDef],
669) {
670 let mut used: std::collections::HashSet<&str> = std::collections::HashSet::new();
671 used.insert(key_schema.partition_key.as_str());
672 if let Some(ref sk) = key_schema.sort_key {
673 used.insert(sk.as_str());
674 }
675 for g in gsis {
676 for k in &g.key_schema {
677 used.insert(k.attribute_name.as_str());
678 }
679 }
680 for lsi in lsi_defs {
681 used.insert(lsi.pk_attr.as_str());
682 if let Some(ref sk) = lsi.sk_attr {
683 used.insert(sk.as_str());
684 }
685 }
686 attr_defs.retain(|d| used.contains(d.attribute_name.as_str()));
687}
688
689fn validate_update_request(request: &UpdateTableRequest) -> Result<()> {
694 let mut errors = Vec::new();
696
697 if let Some(ref pt) = request.provisioned_throughput {
699 if let Some(obj) = pt.as_object() {
700 let wcu = obj.get("WriteCapacityUnits");
701 let rcu = obj.get("ReadCapacityUnits");
702 if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
703 errors.push("Value null at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
704 } else if let Some(w) = wcu.and_then(|v| v.as_i64()) {
705 if w < 1 {
706 errors.push(format!("Value '{}' at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w));
707 }
708 }
709 if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
710 errors.push("Value null at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
711 } else if let Some(r) = rcu.and_then(|v| v.as_i64()) {
712 if r < 1 {
713 errors.push(format!("Value '{}' at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r));
714 }
715 }
716 }
717 }
718
719 if let Some(ref updates) = request.global_secondary_index_updates {
721 for (i, update) in updates.iter().enumerate() {
722 if let Some(ref upd) = update.update {
723 if upd.index_name.len() < 3 {
725 errors.push(format!("Value '{}' at 'globalSecondaryIndexUpdates.{}.member.update.indexName' failed to satisfy constraint: Member must have length greater than or equal to 3", upd.index_name, i + 1));
726 }
727 if !upd.index_name.is_empty()
728 && !upd
729 .index_name
730 .chars()
731 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
732 {
733 errors.push(format!("Value '{}' at 'globalSecondaryIndexUpdates.{}.member.update.indexName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+", upd.index_name, i + 1));
734 }
735 if let Some(ref pt) = upd.provisioned_throughput {
737 let wcu = pt.write_capacity_units;
738 let rcu = pt.read_capacity_units;
739 if wcu.is_none() {
740 errors.push(format!("Value null at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
741 } else if let Some(w) = wcu {
742 if w < 1 {
743 errors.push(format!("Value '{}' at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w, i + 1));
744 }
745 }
746 if rcu.is_none() {
747 errors.push(format!("Value null at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
748 } else if let Some(r) = rcu {
749 if r < 1 {
750 errors.push(format!("Value '{}' at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r, i + 1));
751 }
752 }
753 } else {
754 errors.push(format!("Value null at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput' failed to satisfy constraint: Member must not be null", i + 1));
755 }
756 }
757 }
758 }
759
760 errors.truncate(10);
762
763 if !errors.is_empty() {
764 let prefix = format!(
765 "{} validation error{} detected: ",
766 errors.len(),
767 if errors.len() == 1 { "" } else { "s" }
768 );
769 return Err(DynoxideError::ValidationException(format!(
770 "{}{}",
771 prefix,
772 errors.join("; ")
773 )));
774 }
775
776 if let Some(ref bm) = request.billing_mode {
780 if bm != "PROVISIONED" && bm != "PAY_PER_REQUEST" {
781 return Err(DynoxideError::ValidationException(format!(
782 "1 validation error detected: Value '{}' at 'billingMode' \
783 failed to satisfy constraint: Member must satisfy enum value set: \
784 [PROVISIONED, PAY_PER_REQUEST]",
785 bm
786 )));
787 }
788 }
789
790 if let Some(ref tc) = request.table_class {
792 if tc != "STANDARD" && tc != "STANDARD_INFREQUENT_ACCESS" {
793 return Err(DynoxideError::ValidationException(format!(
794 "1 validation error detected: Value '{tc}' at 'tableClass' failed to satisfy \
795 constraint: Member must satisfy enum value set: \
796 [STANDARD, STANDARD_INFREQUENT_ACCESS]"
797 )));
798 }
799 }
800
801 if request.billing_mode.as_deref() == Some("PAY_PER_REQUEST")
803 && request.provisioned_throughput.is_some()
804 {
805 return Err(DynoxideError::ValidationException(
806 "One or more parameter values were invalid: \
807 Neither ReadCapacityUnits nor WriteCapacityUnits can be \
808 specified when BillingMode is PAY_PER_REQUEST"
809 .to_string(),
810 ));
811 }
812
813 if let Some(ref pt) = request.provisioned_throughput {
815 if let Some(obj) = pt.as_object() {
816 let rcu = obj
817 .get("ReadCapacityUnits")
818 .and_then(|v| v.as_i64())
819 .unwrap_or(0);
820 let wcu = obj
821 .get("WriteCapacityUnits")
822 .and_then(|v| v.as_i64())
823 .unwrap_or(0);
824 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
825 if rcu > MAX_THROUGHPUT {
826 return Err(DynoxideError::ValidationException(format!(
827 "Given value {} for ReadCapacityUnits is out of bounds",
828 rcu
829 )));
830 }
831 if wcu > MAX_THROUGHPUT {
832 return Err(DynoxideError::ValidationException(format!(
833 "Given value {} for WriteCapacityUnits is out of bounds",
834 wcu
835 )));
836 }
837 }
838 }
839
840 let no_config_change = request.provisioned_throughput.is_none()
846 && request.billing_mode.is_none()
847 && request.stream_specification.is_none()
848 && request.deletion_protection_enabled.is_none()
849 && request.table_class.is_none()
850 && request.on_demand_throughput.is_none();
851 let no_gsi_change = request
852 .global_secondary_index_updates
853 .as_ref()
854 .is_none_or(|updates| updates.is_empty());
855 if no_gsi_change && no_config_change {
856 return Err(DynoxideError::ValidationException(
857 "At least one of ProvisionedThroughput, BillingMode, UpdateStreamEnabled, GlobalSecondaryIndexUpdates or SSESpecification or ReplicaUpdates is required".to_string(),
858 ));
859 }
860
861 if let Some(ref updates) = request.global_secondary_index_updates {
863 for update in updates {
865 if update.update.is_none() && update.create.is_none() && update.delete.is_none() {
866 return Err(DynoxideError::ValidationException(
867 "One or more parameter values were invalid: One of GlobalSecondaryIndexUpdate.Update, GlobalSecondaryIndexUpdate.Create, GlobalSecondaryIndexUpdate.Delete must not be null".to_string(),
868 ));
869 }
870 }
871
872 let mut seen_names = std::collections::HashSet::new();
874 for update in updates {
875 let name = if let Some(ref u) = update.update {
876 Some(u.index_name.as_str())
877 } else if let Some(ref c) = update.create {
878 Some(c.index_name.as_str())
879 } else {
880 update.delete.as_ref().map(|d| d.index_name.as_str())
881 };
882 if let Some(name) = name {
883 if !seen_names.insert(name.to_string()) {
884 return Err(DynoxideError::ValidationException(format!(
885 "One or more parameter values were invalid: Only one global secondary index update per index is allowed simultaneously. Index: {}",
886 name
887 )));
888 }
889 }
890 }
891 }
892
893 Ok(())
894}
895
896#[derive(Debug, Clone, Default, Serialize, Deserialize)]
898struct StoredProvisionedThroughput {
899 #[serde(rename = "ReadCapacityUnits")]
900 read_capacity_units: i64,
901 #[serde(rename = "WriteCapacityUnits")]
902 write_capacity_units: i64,
903 #[serde(
904 rename = "LastIncreaseDateTime",
905 skip_serializing_if = "Option::is_none"
906 )]
907 last_increase_date_time: Option<f64>,
908 #[serde(
909 rename = "LastDecreaseDateTime",
910 skip_serializing_if = "Option::is_none"
911 )]
912 last_decrease_date_time: Option<f64>,
913 #[serde(
914 rename = "NumberOfDecreasesToday",
915 skip_serializing_if = "Option::is_none"
916 )]
917 number_of_decreases_today: Option<u64>,
918}
919
920fn parse_current_throughput(meta: &crate::storage::TableMetadata) -> (i64, i64) {
922 parse_stored_throughput(meta)
923 .map(|pt| (pt.read_capacity_units, pt.write_capacity_units))
924 .unwrap_or((0, 0))
925}
926
927fn parse_stored_throughput(
929 meta: &crate::storage::TableMetadata,
930) -> Option<StoredProvisionedThroughput> {
931 meta.provisioned_throughput
932 .as_ref()
933 .and_then(|pt_json| serde_json::from_str(pt_json).ok())
934}
935
936async fn backfill_gsi<S: StorageBackend>(
938 storage: &S,
939 table_name: &str,
940 key_schema: &helpers::KeySchema,
941 gsi_def: &gsi::GsiDef,
942) -> Result<()> {
943 const BATCH_SIZE: usize = 1000;
944 let mut last_pk: Option<String> = None;
945 let mut last_sk: Option<String> = None;
946
947 loop {
948 let items = storage
949 .scan_items(
950 table_name,
951 &crate::storage::ScanParams {
952 limit: Some(BATCH_SIZE),
953 exclusive_start_pk: last_pk.as_deref(),
954 exclusive_start_sk: last_sk.as_deref(),
955 ..Default::default()
956 },
957 )
958 .await?;
959
960 if items.is_empty() {
961 break;
962 }
963
964 let mut rows: Vec<crate::storage_backend::GsiItemRow> = Vec::new();
965 for (pk, sk, item_json) in &items {
966 let item: crate::types::Item = serde_json::from_str(item_json)
967 .map_err(|e| DynoxideError::InternalServerError(format!("Bad item JSON: {e}")))?;
968
969 if let Some((gsi_pk, gsi_sk)) = gsi_def.index_key_strings(&item) {
971 let projected = gsi::build_index_item(
972 &item,
973 gsi_def,
974 &key_schema.partition_key,
975 key_schema.sort_key.as_deref(),
976 );
977 let projected_json = serde_json::to_string(&projected)
978 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
979
980 rows.push(crate::storage_backend::GsiItemRow {
981 gsi_pk,
982 gsi_sk,
983 table_pk: pk.clone(),
984 table_sk: sk.clone(),
985 item_json: projected_json,
986 });
987 }
988 }
989
990 storage
991 .insert_gsi_items(table_name, &gsi_def.index_name, &rows)
992 .await?;
993
994 let last = &items[items.len() - 1];
995 last_pk = Some(last.0.clone());
996 last_sk = Some(last.1.clone());
997
998 if items.len() < BATCH_SIZE {
999 break;
1000 }
1001 }
1002
1003 Ok(())
1004}