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 request: UpdateTableRequest,
160) -> Result<UpdateTableResponse> {
161 validate_update_request(&request)?;
167
168 let meta = helpers::require_table(storage, &request.table_name).await?;
170
171 let current_billing_mode = meta.billing_mode.as_deref().unwrap_or("PROVISIONED");
172
173 if current_billing_mode == "PAY_PER_REQUEST"
177 && request.billing_mode.is_none()
178 && request.provisioned_throughput.is_some()
179 {
180 return Err(DynoxideError::ValidationException(
181 "One or more parameter values were invalid: \
182 Neither ReadCapacityUnits nor WriteCapacityUnits can be \
183 specified when BillingMode is PAY_PER_REQUEST"
184 .to_string(),
185 ));
186 }
187
188 if request.billing_mode.as_deref() == Some("PROVISIONED")
190 && request.provisioned_throughput.is_none()
191 {
192 return Err(DynoxideError::ValidationException(
193 "One or more parameter values were invalid: \
194 ProvisionedThroughput must be specified when BillingMode is PROVISIONED"
195 .to_string(),
196 ));
197 }
198
199 if let Some(ref pt) = request.provisioned_throughput {
201 if let Some(obj) = pt.as_object() {
202 let new_rcu = obj
203 .get("ReadCapacityUnits")
204 .and_then(|v| v.as_i64())
205 .unwrap_or(0);
206 let new_wcu = obj
207 .get("WriteCapacityUnits")
208 .and_then(|v| v.as_i64())
209 .unwrap_or(0);
210
211 let (cur_rcu, cur_wcu) = parse_current_throughput(&meta);
213
214 let billing_mode_unchanged = request.billing_mode.is_none()
215 || (request.billing_mode.as_deref() == Some("PROVISIONED")
216 && current_billing_mode == "PROVISIONED");
217
218 if new_rcu == cur_rcu && new_wcu == cur_wcu && billing_mode_unchanged {
219 return Err(DynoxideError::ValidationException(format!(
220 "The provisioned throughput for the table will not change. \
221 The requested value equals the current value. \
222 Current ReadCapacityUnits provisioned for the table: {}. \
223 Requested ReadCapacityUnits: {}. \
224 Current WriteCapacityUnits provisioned for the table: {}. \
225 Requested WriteCapacityUnits: {}. \
226 Refer to the Amazon DynamoDB Developer Guide for current limits \
227 and how to request higher limits.",
228 cur_rcu, new_rcu, cur_wcu, new_wcu
229 )));
230 }
231 }
232 }
233
234 let mut current_gsis: Vec<GlobalSecondaryIndex> = meta
236 .gsi_definitions
237 .as_ref()
238 .map(|json| serde_json::from_str(json))
239 .transpose()
240 .map_err(|e| DynoxideError::InternalServerError(format!("Bad GSI JSON: {e}")))?
241 .unwrap_or_default();
242
243 if let Some(ref updates) = request.global_secondary_index_updates {
245 for update in updates {
246 if let Some(ref upd) = update.update {
247 if !current_gsis.iter().any(|g| g.index_name == upd.index_name) {
248 return Err(DynoxideError::ValidationException(
251 "This operation cannot be performed with given input values. \
252 Please contact DynamoDB service team for more info: \
253 Action Blocked: IndexUpdate"
254 .to_string(),
255 ));
256 }
257 }
258 }
259 }
260
261 if let Some(ref updates) = request.global_secondary_index_updates {
263 if updates.len() > 5 {
264 return Err(DynoxideError::LimitExceededException(
265 "Subscriber limit exceeded: Only 1 online index can be created or \
266 deleted simultaneously per table"
267 .to_string(),
268 ));
269 }
270 }
271
272 let mut attr_defs: Vec<AttributeDefinition> = serde_json::from_str(&meta.attribute_definitions)
278 .map_err(|e| DynoxideError::InternalServerError(format!("Bad attr defs JSON: {e}")))?;
279
280 if let Some(ref provided) = request.attribute_definitions {
281 for def in provided {
282 if !attr_defs
287 .iter()
288 .any(|d| d.attribute_name == def.attribute_name)
289 {
290 attr_defs.push(def.clone());
291 }
292 }
293 }
294
295 let key_schema = helpers::parse_key_schema(&meta)?;
297
298 if let Some(ref updates) = request.global_secondary_index_updates {
300 for update in updates {
301 if let Some(ref create) = update.create {
302 if current_gsis
303 .iter()
304 .any(|g| g.index_name == create.index_name)
305 {
306 return Err(DynoxideError::ValidationException(format!(
307 "One or more parameter values were invalid: \
308 Index already exists: {}",
309 create.index_name
310 )));
311 }
312 let gsi_def = GlobalSecondaryIndex {
313 index_name: create.index_name.clone(),
314 key_schema: create.key_schema.clone(),
315 projection: create.projection.clone(),
316 provisioned_throughput: None,
317 };
318 validation::validate_gsi(&gsi_def, &attr_defs)?;
319 }
320 if let Some(ref delete) = update.delete {
321 if !current_gsis
322 .iter()
323 .any(|g| g.index_name == delete.index_name)
324 {
325 return Err(DynoxideError::ResourceNotFoundException(format!(
326 "Requested resource not found: Table: {} not found",
327 delete.index_name
328 )));
329 }
330 }
331 }
332 }
333
334 let now = {
338 use std::sync::atomic::{AtomicU64, Ordering};
339 static LAST_TS: AtomicU64 = AtomicU64::new(0);
340 let wall = web_time::SystemTime::now()
341 .duration_since(web_time::UNIX_EPOCH)
342 .unwrap_or_default()
343 .as_secs_f64();
344 loop {
345 let prev_bits = LAST_TS.load(Ordering::SeqCst);
346 let prev_f = f64::from_bits(prev_bits);
347 let candidate = if wall > prev_f { wall } else { prev_f + 0.001 };
348 let candidate_bits = candidate.to_bits();
349 if LAST_TS
350 .compare_exchange(
351 prev_bits,
352 candidate_bits,
353 Ordering::SeqCst,
354 Ordering::SeqCst,
355 )
356 .is_ok()
357 {
358 break candidate;
359 }
360 }
361 };
362
363 let (cur_rcu, cur_wcu) = parse_current_throughput(&meta);
364 let is_pt_update = request.provisioned_throughput.is_some();
365 let (new_rcu, new_wcu) = if let Some(ref pt) = request.provisioned_throughput {
366 let obj = pt.as_object();
367 (
368 obj.and_then(|o| o.get("ReadCapacityUnits"))
369 .and_then(|v| v.as_i64())
370 .unwrap_or(0),
371 obj.and_then(|o| o.get("WriteCapacityUnits"))
372 .and_then(|v| v.as_i64())
373 .unwrap_or(0),
374 )
375 } else {
376 (cur_rcu, cur_wcu)
377 };
378
379 let is_increase = new_rcu > cur_rcu || new_wcu > cur_wcu;
380 let is_decrease = new_rcu < cur_rcu || new_wcu < cur_wcu;
381
382 helpers::with_write_transaction(storage, async {
384 if let Some(ref updates) = request.global_secondary_index_updates {
385 for update in updates {
386 if let Some(ref create) = update.create {
387 let gsi_def = GlobalSecondaryIndex {
388 index_name: create.index_name.clone(),
389 key_schema: create.key_schema.clone(),
390 projection: create.projection.clone(),
391 provisioned_throughput: None,
392 };
393
394 storage
395 .create_gsi_table(&request.table_name, &create.index_name)
396 .await?;
397
398 let gsi_p = gsi::gsi_to_def(&gsi_def)?;
399 backfill_gsi(storage, &request.table_name, &key_schema, &gsi_p).await?;
400
401 current_gsis.push(gsi_def);
402 }
403
404 if let Some(ref delete) = update.delete {
405 storage
406 .drop_gsi_table(&request.table_name, &delete.index_name)
407 .await?;
408 current_gsis.retain(|g| g.index_name != delete.index_name);
409 }
410 }
411 }
412
413 let lsi_defs = crate::actions::lsi::parse_lsi_defs(&meta)?;
417 reconcile_attribute_definitions(&mut attr_defs, &key_schema, ¤t_gsis, &lsi_defs);
418
419 let attr_defs_json = serde_json::to_string(&attr_defs)
421 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
422 let gsi_json = if current_gsis.is_empty() {
423 None
424 } else {
425 Some(
426 serde_json::to_string(¤t_gsis)
427 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?,
428 )
429 };
430
431 storage
432 .update_table_metadata(&request.table_name, &attr_defs_json, gsi_json.as_deref())
433 .await?;
434
435 if is_pt_update {
437 let prev = parse_stored_throughput(&meta);
438 let mut stored = StoredProvisionedThroughput {
439 read_capacity_units: new_rcu,
440 write_capacity_units: new_wcu,
441 last_increase_date_time: prev.as_ref().and_then(|p| p.last_increase_date_time),
442 last_decrease_date_time: prev.as_ref().and_then(|p| p.last_decrease_date_time),
443 number_of_decreases_today: prev
444 .as_ref()
445 .and_then(|p| p.number_of_decreases_today)
446 .or(Some(0)),
447 };
448 if is_increase {
449 stored.last_increase_date_time = Some(now);
450 }
451 if is_decrease {
452 stored.last_decrease_date_time = Some(now);
453 stored.number_of_decreases_today =
454 Some(stored.number_of_decreases_today.unwrap_or(0) + 1);
455 }
456 let pt_json = serde_json::to_string(&stored)
457 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
458 storage
459 .update_provisioned_throughput(&request.table_name, &pt_json)
460 .await?;
461 }
462
463 if let Some(enabled) = request.deletion_protection_enabled {
465 storage
466 .update_deletion_protection(&request.table_name, enabled)
467 .await?;
468 }
469
470 if let Some(ref table_class) = request.table_class {
472 storage
473 .update_table_class(&request.table_name, table_class)
474 .await?;
475 }
476
477 if let Some(ref on_demand) = request.on_demand_throughput {
479 let json = serde_json::to_string(on_demand)
480 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
481 storage
482 .update_on_demand_throughput(&request.table_name, &json)
483 .await?;
484 }
485
486 if let Some(ref billing_mode) = request.billing_mode {
488 storage
489 .update_billing_mode(&request.table_name, billing_mode)
490 .await?;
491 if billing_mode == "PAY_PER_REQUEST" {
492 storage
494 .clear_provisioned_throughput(&request.table_name)
495 .await?;
496 }
497 }
498
499 if let Some(ref spec) = request.stream_specification {
501 if spec.stream_enabled {
502 let view_type = spec
503 .stream_view_type
504 .as_deref()
505 .unwrap_or("NEW_AND_OLD_IMAGES");
506 let label = streams::generate_stream_label(storage.clock());
507 storage
508 .enable_stream(&request.table_name, view_type, &label)
509 .await?;
510 } else {
511 storage.disable_stream(&request.table_name).await?;
512 }
513 }
514
515 Ok(())
516 })
517 .await?;
518
519 let updated_meta = helpers::require_table(storage, &request.table_name).await?;
521 let mut desc = build_table_description(&updated_meta, Some(0), Some(0));
522
523 if is_pt_update {
525 desc.table_status = "UPDATING".to_string();
526
527 let stored = parse_stored_throughput(&updated_meta);
530 if let Some(ref mut pt) = desc.provisioned_throughput {
531 pt.read_capacity_units = cur_rcu as u64;
532 pt.write_capacity_units = cur_wcu as u64;
533 if let Some(ref s) = stored {
534 pt.last_increase_date_time = s.last_increase_date_time;
535 pt.last_decrease_date_time = s.last_decrease_date_time;
536 pt.number_of_decreases_today = s.number_of_decreases_today.unwrap_or(0);
537 }
538 }
539 }
540
541 Ok(UpdateTableResponse {
542 table_description: desc,
543 })
544}
545
546fn reconcile_attribute_definitions(
552 attr_defs: &mut Vec<AttributeDefinition>,
553 key_schema: &helpers::KeySchema,
554 gsis: &[GlobalSecondaryIndex],
555 lsi_defs: &[crate::actions::lsi::LsiDef],
556) {
557 let mut used: std::collections::HashSet<&str> = std::collections::HashSet::new();
558 used.insert(key_schema.partition_key.as_str());
559 if let Some(ref sk) = key_schema.sort_key {
560 used.insert(sk.as_str());
561 }
562 for g in gsis {
563 for k in &g.key_schema {
564 used.insert(k.attribute_name.as_str());
565 }
566 }
567 for lsi in lsi_defs {
568 used.insert(lsi.pk_attr.as_str());
569 if let Some(ref sk) = lsi.sk_attr {
570 used.insert(sk.as_str());
571 }
572 }
573 attr_defs.retain(|d| used.contains(d.attribute_name.as_str()));
574}
575
576fn validate_update_request(request: &UpdateTableRequest) -> Result<()> {
581 let mut errors = Vec::new();
583
584 if let Some(ref pt) = request.provisioned_throughput {
586 if let Some(obj) = pt.as_object() {
587 let wcu = obj.get("WriteCapacityUnits");
588 let rcu = obj.get("ReadCapacityUnits");
589 if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
590 errors.push("Value null at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
591 } else if let Some(w) = wcu.and_then(|v| v.as_i64()) {
592 if w < 1 {
593 errors.push(format!("Value '{}' at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w));
594 }
595 }
596 if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
597 errors.push("Value null at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
598 } else if let Some(r) = rcu.and_then(|v| v.as_i64()) {
599 if r < 1 {
600 errors.push(format!("Value '{}' at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r));
601 }
602 }
603 }
604 }
605
606 if let Some(ref updates) = request.global_secondary_index_updates {
608 for (i, update) in updates.iter().enumerate() {
609 if let Some(ref upd) = update.update {
610 if upd.index_name.len() < 3 {
612 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));
613 }
614 if !upd.index_name.is_empty()
615 && !upd
616 .index_name
617 .chars()
618 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
619 {
620 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));
621 }
622 if let Some(ref pt) = upd.provisioned_throughput {
624 let wcu = pt.write_capacity_units;
625 let rcu = pt.read_capacity_units;
626 if wcu.is_none() {
627 errors.push(format!("Value null at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
628 } else if let Some(w) = wcu {
629 if w < 1 {
630 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));
631 }
632 }
633 if rcu.is_none() {
634 errors.push(format!("Value null at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
635 } else if let Some(r) = rcu {
636 if r < 1 {
637 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));
638 }
639 }
640 } else {
641 errors.push(format!("Value null at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput' failed to satisfy constraint: Member must not be null", i + 1));
642 }
643 }
644 }
645 }
646
647 errors.truncate(10);
649
650 if !errors.is_empty() {
651 let prefix = format!(
652 "{} validation error{} detected: ",
653 errors.len(),
654 if errors.len() == 1 { "" } else { "s" }
655 );
656 return Err(DynoxideError::ValidationException(format!(
657 "{}{}",
658 prefix,
659 errors.join("; ")
660 )));
661 }
662
663 if let Some(ref bm) = request.billing_mode {
667 if bm != "PROVISIONED" && bm != "PAY_PER_REQUEST" {
668 return Err(DynoxideError::ValidationException(format!(
669 "1 validation error detected: Value '{}' at 'billingMode' \
670 failed to satisfy constraint: Member must satisfy enum value set: \
671 [PROVISIONED, PAY_PER_REQUEST]",
672 bm
673 )));
674 }
675 }
676
677 if let Some(ref tc) = request.table_class {
679 if tc != "STANDARD" && tc != "STANDARD_INFREQUENT_ACCESS" {
680 return Err(DynoxideError::ValidationException(format!(
681 "1 validation error detected: Value '{tc}' at 'tableClass' failed to satisfy \
682 constraint: Member must satisfy enum value set: \
683 [STANDARD, STANDARD_INFREQUENT_ACCESS]"
684 )));
685 }
686 }
687
688 if request.billing_mode.as_deref() == Some("PAY_PER_REQUEST")
690 && request.provisioned_throughput.is_some()
691 {
692 return Err(DynoxideError::ValidationException(
693 "One or more parameter values were invalid: \
694 Neither ReadCapacityUnits nor WriteCapacityUnits can be \
695 specified when BillingMode is PAY_PER_REQUEST"
696 .to_string(),
697 ));
698 }
699
700 if let Some(ref pt) = request.provisioned_throughput {
702 if let Some(obj) = pt.as_object() {
703 let rcu = obj
704 .get("ReadCapacityUnits")
705 .and_then(|v| v.as_i64())
706 .unwrap_or(0);
707 let wcu = obj
708 .get("WriteCapacityUnits")
709 .and_then(|v| v.as_i64())
710 .unwrap_or(0);
711 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
712 if rcu > MAX_THROUGHPUT {
713 return Err(DynoxideError::ValidationException(format!(
714 "Given value {} for ReadCapacityUnits is out of bounds",
715 rcu
716 )));
717 }
718 if wcu > MAX_THROUGHPUT {
719 return Err(DynoxideError::ValidationException(format!(
720 "Given value {} for WriteCapacityUnits is out of bounds",
721 wcu
722 )));
723 }
724 }
725 }
726
727 let no_config_change = request.provisioned_throughput.is_none()
733 && request.billing_mode.is_none()
734 && request.stream_specification.is_none()
735 && request.deletion_protection_enabled.is_none()
736 && request.table_class.is_none()
737 && request.on_demand_throughput.is_none();
738 let no_gsi_change = request
739 .global_secondary_index_updates
740 .as_ref()
741 .is_none_or(|updates| updates.is_empty());
742 if no_gsi_change && no_config_change {
743 return Err(DynoxideError::ValidationException(
744 "At least one of ProvisionedThroughput, BillingMode, UpdateStreamEnabled, GlobalSecondaryIndexUpdates or SSESpecification or ReplicaUpdates is required".to_string(),
745 ));
746 }
747
748 if let Some(ref updates) = request.global_secondary_index_updates {
750 for update in updates {
752 if update.update.is_none() && update.create.is_none() && update.delete.is_none() {
753 return Err(DynoxideError::ValidationException(
754 "One or more parameter values were invalid: One of GlobalSecondaryIndexUpdate.Update, GlobalSecondaryIndexUpdate.Create, GlobalSecondaryIndexUpdate.Delete must not be null".to_string(),
755 ));
756 }
757 }
758
759 let mut seen_names = std::collections::HashSet::new();
761 for update in updates {
762 let name = if let Some(ref u) = update.update {
763 Some(u.index_name.as_str())
764 } else if let Some(ref c) = update.create {
765 Some(c.index_name.as_str())
766 } else {
767 update.delete.as_ref().map(|d| d.index_name.as_str())
768 };
769 if let Some(name) = name {
770 if !seen_names.insert(name.to_string()) {
771 return Err(DynoxideError::ValidationException(format!(
772 "One or more parameter values were invalid: Only one global secondary index update per index is allowed simultaneously. Index: {}",
773 name
774 )));
775 }
776 }
777 }
778 }
779
780 Ok(())
781}
782
783#[derive(Debug, Clone, Default, Serialize, Deserialize)]
785struct StoredProvisionedThroughput {
786 #[serde(rename = "ReadCapacityUnits")]
787 read_capacity_units: i64,
788 #[serde(rename = "WriteCapacityUnits")]
789 write_capacity_units: i64,
790 #[serde(
791 rename = "LastIncreaseDateTime",
792 skip_serializing_if = "Option::is_none"
793 )]
794 last_increase_date_time: Option<f64>,
795 #[serde(
796 rename = "LastDecreaseDateTime",
797 skip_serializing_if = "Option::is_none"
798 )]
799 last_decrease_date_time: Option<f64>,
800 #[serde(
801 rename = "NumberOfDecreasesToday",
802 skip_serializing_if = "Option::is_none"
803 )]
804 number_of_decreases_today: Option<u64>,
805}
806
807fn parse_current_throughput(meta: &crate::storage::TableMetadata) -> (i64, i64) {
809 parse_stored_throughput(meta)
810 .map(|pt| (pt.read_capacity_units, pt.write_capacity_units))
811 .unwrap_or((0, 0))
812}
813
814fn parse_stored_throughput(
816 meta: &crate::storage::TableMetadata,
817) -> Option<StoredProvisionedThroughput> {
818 meta.provisioned_throughput
819 .as_ref()
820 .and_then(|pt_json| serde_json::from_str(pt_json).ok())
821}
822
823async fn backfill_gsi<S: StorageBackend>(
825 storage: &S,
826 table_name: &str,
827 key_schema: &helpers::KeySchema,
828 gsi_def: &gsi::GsiDef,
829) -> Result<()> {
830 const BATCH_SIZE: usize = 1000;
831 let mut last_pk: Option<String> = None;
832 let mut last_sk: Option<String> = None;
833
834 loop {
835 let items = storage
836 .scan_items(
837 table_name,
838 &crate::storage::ScanParams {
839 limit: Some(BATCH_SIZE),
840 exclusive_start_pk: last_pk.as_deref(),
841 exclusive_start_sk: last_sk.as_deref(),
842 ..Default::default()
843 },
844 )
845 .await?;
846
847 if items.is_empty() {
848 break;
849 }
850
851 let mut rows: Vec<crate::storage_backend::GsiItemRow> = Vec::new();
852 for (pk, sk, item_json) in &items {
853 let item: crate::types::Item = serde_json::from_str(item_json)
854 .map_err(|e| DynoxideError::InternalServerError(format!("Bad item JSON: {e}")))?;
855
856 if let Some((gsi_pk, gsi_sk)) = gsi_def.index_key_strings(&item) {
858 let projected = gsi::build_index_item(
859 &item,
860 gsi_def,
861 &key_schema.partition_key,
862 key_schema.sort_key.as_deref(),
863 );
864 let projected_json = serde_json::to_string(&projected)
865 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
866
867 rows.push(crate::storage_backend::GsiItemRow {
868 gsi_pk,
869 gsi_sk,
870 table_pk: pk.clone(),
871 table_sk: sk.clone(),
872 item_json: projected_json,
873 });
874 }
875 }
876
877 storage
878 .insert_gsi_items(table_name, &gsi_def.index_name, &rows)
879 .await?;
880
881 let last = &items[items.len() - 1];
882 last_pk = Some(last.0.clone());
883 last_sk = Some(last.1.clone());
884
885 if items.len() < BATCH_SIZE {
886 break;
887 }
888 }
889
890 Ok(())
891}