1use crate::actions::create_table::StreamSpecification;
2use crate::actions::vector_lifecycle::VectorIndexLifecycle;
3use crate::actions::{TableDescription, build_table_description};
4use crate::actions::{gsi, helpers};
5use crate::errors::{DynoxideError, Result};
6use crate::storage_backend::StorageBackend;
7use crate::streams;
8use crate::types::{
9 AttributeDefinition, GlobalSecondaryIndex, Item, KeySchemaElement, Projection, VectorIndex,
10};
11use crate::validation;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Default, Deserialize)]
16struct UpdateTableRequestRaw {
17 #[serde(rename = "TableName", default)]
18 table_name: Option<String>,
19
20 #[serde(rename = "AttributeDefinitions", default)]
21 attribute_definitions: Option<Vec<AttributeDefinition>>,
22
23 #[serde(rename = "GlobalSecondaryIndexUpdates", default)]
24 global_secondary_index_updates: Option<Vec<GlobalSecondaryIndexUpdate>>,
25
26 #[serde(rename = "VectorIndexUpdates", default)]
30 vector_index_updates: Option<serde_json::Value>,
31
32 #[serde(rename = "StreamSpecification", default)]
33 stream_specification: Option<StreamSpecification>,
34
35 #[serde(rename = "DeletionProtectionEnabled", default)]
36 deletion_protection_enabled: Option<bool>,
37
38 #[serde(rename = "ProvisionedThroughput", default)]
39 provisioned_throughput: Option<serde_json::Value>,
40
41 #[serde(rename = "BillingMode", default)]
42 billing_mode: Option<String>,
43
44 #[serde(rename = "TableClass", default)]
45 table_class: Option<String>,
46
47 #[serde(rename = "OnDemandThroughput", default)]
48 on_demand_throughput: Option<crate::types::OnDemandThroughput>,
49}
50
51#[derive(Debug, Default)]
52pub struct UpdateTableRequest {
53 pub table_name: String,
54 pub attribute_definitions: Option<Vec<AttributeDefinition>>,
55 pub global_secondary_index_updates: Option<Vec<GlobalSecondaryIndexUpdate>>,
56 pub vector_index_updates: Option<Vec<VectorIndexUpdate>>,
57 pub stream_specification: Option<StreamSpecification>,
58 pub deletion_protection_enabled: Option<bool>,
59 pub provisioned_throughput: Option<serde_json::Value>,
60 pub billing_mode: Option<String>,
61 pub table_class: Option<String>,
62 pub on_demand_throughput: Option<crate::types::OnDemandThroughput>,
63}
64
65impl<'de> serde::Deserialize<'de> for UpdateTableRequest {
66 fn deserialize<D: serde::Deserializer<'de>>(
67 deserializer: D,
68 ) -> std::result::Result<Self, D::Error> {
69 let raw = UpdateTableRequestRaw::deserialize(deserializer)?;
70
71 if raw.table_name.is_none() || raw.table_name.as_deref() == Some("") {
73 let msg = if raw.table_name.is_none() {
74 "The parameter 'TableName' is required but was not present in the request"
75 } else {
76 "TableName must be at least 3 characters long and at most 255 characters long"
77 };
78 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
79 }
80
81 let table_name = raw.table_name.unwrap_or_default();
82
83 if table_name.len() < 3 || table_name.len() > 255 {
85 return Err(serde::de::Error::custom(
86 "VALIDATION:TableName must be at least 3 characters long and at most 255 characters long",
87 ));
88 }
89
90 let mut errors = Vec::new();
92
93 if !table_name
94 .chars()
95 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
96 {
97 errors.push(format!(
98 "Value '{}' at 'tableName' failed to satisfy constraint: \
99 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
100 table_name
101 ));
102 }
103
104 if let Some(msg) = crate::validation::format_validation_errors(&errors) {
105 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
106 }
107
108 if let Some(ref vix_val) = raw.vector_index_updates {
118 let mut vix_errors = Vec::new();
119 collect_vix_update_errors(vix_val, &mut vix_errors);
120 if !vix_errors.is_empty() {
121 let mut errors = Vec::new();
122 if let Some(ref gsi_updates) = raw.global_secondary_index_updates {
123 collect_gsi_update_errors(gsi_updates, &mut errors);
124 }
125 errors.append(&mut vix_errors);
126 errors.truncate(10);
127 let msg = format!(
128 "{} validation error{} detected: {}",
129 errors.len(),
130 if errors.len() == 1 { "" } else { "s" },
131 errors.join("; ")
132 );
133 return Err(serde::de::Error::custom(format!("VALIDATION:{}", msg)));
134 }
135 }
136
137 let vector_index_updates = raw
138 .vector_index_updates
139 .as_ref()
140 .map(parse_vector_index_updates)
141 .transpose()
142 .map_err(serde::de::Error::custom)?;
143
144 Ok(UpdateTableRequest {
145 table_name,
146 attribute_definitions: raw.attribute_definitions,
147 global_secondary_index_updates: raw.global_secondary_index_updates,
148 vector_index_updates,
149 stream_specification: raw.stream_specification,
150 deletion_protection_enabled: raw.deletion_protection_enabled,
151 provisioned_throughput: raw.provisioned_throughput,
152 billing_mode: raw.billing_mode,
153 table_class: raw.table_class,
154 on_demand_throughput: raw.on_demand_throughput,
155 })
156 }
157}
158
159#[derive(Debug, Default, Deserialize)]
160pub struct GlobalSecondaryIndexUpdate {
161 #[serde(rename = "Update", default)]
162 pub update: Option<UpdateGsiAction>,
163
164 #[serde(rename = "Create", default)]
165 pub create: Option<CreateGsiAction>,
166
167 #[serde(rename = "Delete", default)]
168 pub delete: Option<DeleteGsiAction>,
169}
170
171#[derive(Debug, Default, Deserialize)]
172pub struct UpdateGsiAction {
173 #[serde(rename = "IndexName")]
174 pub index_name: String,
175
176 #[serde(rename = "ProvisionedThroughput", default)]
177 pub provisioned_throughput: Option<crate::types::ProvisionedThroughput>,
178}
179
180#[derive(Debug, Default, Deserialize)]
181pub struct CreateGsiAction {
182 #[serde(rename = "IndexName")]
183 pub index_name: String,
184
185 #[serde(rename = "KeySchema")]
186 pub key_schema: Vec<KeySchemaElement>,
187
188 #[serde(rename = "Projection")]
189 pub projection: Projection,
190}
191
192#[derive(Debug, Default, Deserialize)]
193pub struct DeleteGsiAction {
194 #[serde(rename = "IndexName")]
195 pub index_name: String,
196}
197
198#[derive(Debug, Default)]
203pub struct VectorIndexUpdate {
204 pub create: Option<VectorIndex>,
207 pub delete: Option<DeleteVectorIndexAction>,
208}
209
210#[derive(Debug, Default, Deserialize)]
211pub struct DeleteVectorIndexAction {
212 #[serde(rename = "IndexName")]
213 pub index_name: String,
214}
215
216fn collect_vix_update_errors(val: &serde_json::Value, errors: &mut Vec<String>) {
221 let Some(arr) = val.as_array() else {
222 return;
223 };
224 for (i, elem) in arr.iter().enumerate().take(10) {
225 let Some(obj) = elem.as_object() else {
226 continue;
227 };
228 if let Some(create) = obj.get("Create").and_then(|v| v.as_object()) {
229 crate::actions::create_table::collect_vix_obj_errors(
230 create,
231 &format!("vectorIndexUpdates.{}.member.create", i + 1),
232 errors,
233 );
234 }
235 }
236}
237
238#[cfg(feature = "mcp-server")]
243pub(crate) fn vector_index_updates_request_model_error(val: &serde_json::Value) -> Option<String> {
244 let mut errors = Vec::new();
245 collect_vix_update_errors(val, &mut errors);
246 errors.truncate(10);
247 if errors.is_empty() {
248 return None;
249 }
250 Some(format!(
251 "{} validation error{} detected: {}",
252 errors.len(),
253 if errors.len() == 1 { "" } else { "s" },
254 errors.join("; ")
255 ))
256}
257
258pub(crate) fn parse_vector_index_updates(
266 val: &serde_json::Value,
267) -> std::result::Result<Vec<VectorIndexUpdate>, String> {
268 let arr = val
269 .as_array()
270 .ok_or_else(|| "Unexpected field type".to_string())?;
271 let mut out = Vec::with_capacity(arr.len());
272 for elem in arr {
273 let obj = elem
274 .as_object()
275 .ok_or_else(|| "Unexpected value type in payload".to_string())?;
276 let mut update = VectorIndexUpdate::default();
277 if let Some(create) = obj.get("Create").filter(|v| !v.is_null()) {
284 let mut create = create.clone();
285 if let Some(create_obj) = create.as_object_mut() {
286 crate::actions::create_table::normalise_vix_dimensions_obj(create_obj);
287 }
288 update.create = Some(serde_json::from_value(create).map_err(|e| e.to_string())?);
289 }
290 if let Some(delete) = obj.get("Delete").filter(|v| !v.is_null()) {
291 update.delete =
292 Some(serde_json::from_value(delete.clone()).map_err(|e| e.to_string())?);
293 }
294 out.push(update);
295 }
296 Ok(out)
297}
298
299#[derive(Debug, Default, Serialize)]
300pub struct UpdateTableResponse {
301 #[serde(rename = "TableDescription")]
302 pub table_description: TableDescription,
303}
304
305const ONE_ONLINE_INDEX: &str = "Subscriber limit exceeded: Only 1 online index can be created or deleted \
308 simultaneously per table";
309
310pub async fn execute<S: StorageBackend>(
311 storage: &S,
312 mut request: UpdateTableRequest,
313 lifecycle: &VectorIndexLifecycle,
314) -> Result<UpdateTableResponse> {
315 if request.on_demand_throughput.as_ref().is_some_and(|odt| {
323 odt.max_read_request_units.is_none() && odt.max_write_request_units.is_none()
324 }) {
325 request.on_demand_throughput = None;
326 }
327
328 validate_update_request(&request)?;
332
333 let meta = helpers::require_table(storage, &request.table_name).await?;
335
336 let current_billing_mode = meta.billing_mode.as_deref().unwrap_or("PROVISIONED");
337
338 let mut current_vixs: Vec<VectorIndex> = meta
341 .vector_index_definitions
342 .as_ref()
343 .map(|json| serde_json::from_str(json))
344 .transpose()
345 .map_err(|e| DynoxideError::InternalServerError(format!("Bad vector index JSON: {e}")))?
346 .unwrap_or_default();
347
348 if current_billing_mode == "PAY_PER_REQUEST"
352 && request.billing_mode.is_none()
353 && request.provisioned_throughput.is_some()
354 {
355 return Err(DynoxideError::ValidationException(
356 "One or more parameter values were invalid: \
357 Neither ReadCapacityUnits nor WriteCapacityUnits can be \
358 specified when BillingMode is PAY_PER_REQUEST"
359 .to_string(),
360 ));
361 }
362
363 if request.billing_mode.as_deref() == Some("PROVISIONED")
365 && request.provisioned_throughput.is_none()
366 {
367 return Err(DynoxideError::ValidationException(
368 "One or more parameter values were invalid: \
369 ProvisionedThroughput must be specified when BillingMode is PROVISIONED"
370 .to_string(),
371 ));
372 }
373
374 if request.billing_mode.as_deref() == Some("PROVISIONED") && !current_vixs.is_empty() {
380 return Err(DynoxideError::ValidationException(
381 "One or more parameter values were invalid: Tables with vector indexes must \
382 be in PAY_PER_REQUEST mode"
383 .to_string(),
384 ));
385 }
386
387 if let Some(ref odt) = request.on_demand_throughput {
395 let target_is_provisioned = match request.billing_mode.as_deref() {
396 Some("PAY_PER_REQUEST") => false,
397 Some(_) => true,
398 None => current_billing_mode == "PROVISIONED",
399 };
400 let members = [
401 ("MaxReadRequestUnits", odt.max_read_request_units),
402 ("MaxWriteRequestUnits", odt.max_write_request_units),
403 ];
404 if target_is_provisioned
405 && let Some((member, _)) = members.iter().find(|(_, v)| v.is_some())
406 {
407 return Err(DynoxideError::ValidationException(format!(
408 "One or more parameter values were invalid: {member} for \
409 OnDemandThroughput cannot be specified when the table BillingMode \
410 is PROVISIONED"
411 )));
412 }
413 for (member, value) in members {
416 if value.is_some_and(|v| v == 0 || v < -1) {
417 return Err(DynoxideError::ValidationException(format!(
418 "One or more parameter values were invalid: Requested {member} for \
419 OnDemandThroughput for table is outside of valid range"
420 )));
421 }
422 }
423 }
424
425 if let Some(ref pt) = request.provisioned_throughput
427 && let Some(obj) = pt.as_object()
428 {
429 let new_rcu = obj
430 .get("ReadCapacityUnits")
431 .and_then(|v| v.as_i64())
432 .unwrap_or(0);
433 let new_wcu = obj
434 .get("WriteCapacityUnits")
435 .and_then(|v| v.as_i64())
436 .unwrap_or(0);
437
438 let (cur_rcu, cur_wcu) = parse_current_throughput(&meta);
440
441 let billing_mode_unchanged = request.billing_mode.is_none()
442 || (request.billing_mode.as_deref() == Some("PROVISIONED")
443 && current_billing_mode == "PROVISIONED");
444
445 if new_rcu == cur_rcu && new_wcu == cur_wcu && billing_mode_unchanged {
446 return Err(DynoxideError::ValidationException(format!(
447 "The provisioned throughput for the table will not change. \
448 The requested value equals the current value. \
449 Current ReadCapacityUnits provisioned for the table: {}. \
450 Requested ReadCapacityUnits: {}. \
451 Current WriteCapacityUnits provisioned for the table: {}. \
452 Requested WriteCapacityUnits: {}. \
453 Refer to the Amazon DynamoDB Developer Guide for current limits \
454 and how to request higher limits.",
455 cur_rcu, new_rcu, cur_wcu, new_wcu
456 )));
457 }
458 }
459
460 if request.stream_specification.is_some() && !storage.supports_streams() {
465 return Err(crate::storage_backend::BackendError::Unsupported {
466 capability: "streams",
467 }
468 .into());
469 }
470
471 let mut current_gsis: Vec<GlobalSecondaryIndex> = meta
473 .gsi_definitions
474 .as_ref()
475 .map(|json| serde_json::from_str(json))
476 .transpose()
477 .map_err(|e| DynoxideError::InternalServerError(format!("Bad GSI JSON: {e}")))?
478 .unwrap_or_default();
479
480 let lsi_defs = crate::actions::lsi::parse_lsi_defs(&meta)?;
483
484 if let Some(ref updates) = request.global_secondary_index_updates {
486 for update in updates {
487 if let Some(ref upd) = update.update
488 && !current_gsis.iter().any(|g| g.index_name == upd.index_name)
489 {
490 return Err(DynoxideError::ValidationException(
493 "This operation cannot be performed with given input values. \
494 Please contact DynamoDB service team for more info: \
495 Action Blocked: IndexUpdate"
496 .to_string(),
497 ));
498 }
499 }
500 }
501
502 if let Some(ref updates) = request.global_secondary_index_updates
504 && updates.len() > 5
505 {
506 return Err(DynoxideError::LimitExceededException(
507 "Subscriber limit exceeded: Only 1 online index can be created or \
508 deleted simultaneously per table"
509 .to_string(),
510 ));
511 }
512
513 let mut attr_defs: Vec<AttributeDefinition> = serde_json::from_str(&meta.attribute_definitions)
519 .map_err(|e| DynoxideError::InternalServerError(format!("Bad attr defs JSON: {e}")))?;
520
521 if let Some(ref provided) = request.attribute_definitions {
522 for def in provided {
523 if !attr_defs
528 .iter()
529 .any(|d| d.attribute_name == def.attribute_name)
530 {
531 attr_defs.push(def.clone());
532 }
533 }
534 }
535
536 let key_schema = helpers::parse_key_schema(&meta)?;
538
539 if let Some(ref updates) = request.global_secondary_index_updates {
541 for update in updates {
542 if let Some(ref create) = update.create {
543 if current_gsis
544 .iter()
545 .any(|g| g.index_name == create.index_name)
546 {
547 return Err(DynoxideError::ValidationException(format!(
548 "One or more parameter values were invalid: \
549 Index already exists: {}",
550 create.index_name
551 )));
552 }
553 if current_vixs
558 .iter()
559 .any(|v| v.index_name == create.index_name)
560 {
561 return Err(DynoxideError::ValidationException(
562 "Attempting to create an index which already exists".to_string(),
563 ));
564 }
565 for elem in &create.key_schema {
575 let Some(vix) = current_vixs
576 .iter()
577 .find(|v| v.vector_attribute.attribute_name == elem.attribute_name)
578 else {
579 continue;
580 };
581 let Some(def) = request
582 .attribute_definitions
583 .as_deref()
584 .unwrap_or(&[])
585 .iter()
586 .find(|d| d.attribute_name == elem.attribute_name)
587 else {
588 continue;
589 };
590 let type_letter = match def.attribute_type {
591 crate::types::ScalarAttributeType::S => "S",
592 crate::types::ScalarAttributeType::N => "N",
593 crate::types::ScalarAttributeType::B => "B",
594 };
595 let key_type = match elem.key_type {
596 crate::types::KeyType::HASH => "HASH",
597 crate::types::KeyType::RANGE => "RANGE",
598 };
599 return Err(DynoxideError::ValidationException(format!(
600 "One or more parameter values were invalid: Attributes cannot be \
601 redefined. Please check that your attribute has the same type as \
602 previously defined. Existing schema: \
603 VectorIndexSchema:[VectorAttribute: key{{{attr}:L:{dims}}}] \
604 New schema: Schema:[SchemaElement: key{{{attr}:{type_letter}:{key_type}}}]",
605 attr = elem.attribute_name,
606 dims = vix.dimensions,
607 )));
608 }
609 let gsi_def = GlobalSecondaryIndex {
610 index_name: create.index_name.clone(),
611 key_schema: create.key_schema.clone(),
612 projection: create.projection.clone(),
613 provisioned_throughput: None,
614 };
615 validation::validate_gsi(
620 &gsi_def,
621 request.attribute_definitions.as_deref().unwrap_or(&[]),
622 )?;
623 }
624 if let Some(ref delete) = update.delete
625 && !current_gsis
626 .iter()
627 .any(|g| g.index_name == delete.index_name)
628 {
629 return Err(DynoxideError::ResourceNotFoundException(format!(
630 "Requested resource not found: Table: {} not found",
631 delete.index_name
632 )));
633 }
634 }
635 }
636
637 if let Some(ref updates) = request.vector_index_updates {
639 if updates.len() > 1 {
645 return Err(DynoxideError::LimitExceededException(
646 ONE_ONLINE_INDEX.to_string(),
647 ));
648 }
649 let creating = lifecycle.phases_of(
660 storage,
661 &request.table_name,
662 current_vixs.iter().map(|v| v.index_name.as_str()),
663 );
664 let targeted = updates.first().and_then(|u| u.delete.as_ref());
665 let cancels_the_creating_index =
666 targeted.is_some_and(|d| creating.get(&d.index_name).is_creating());
667 if !updates.is_empty() && creating.any_creating() && !cancels_the_creating_index {
668 return Err(DynoxideError::LimitExceededException(
669 ONE_ONLINE_INDEX.to_string(),
670 ));
671 }
672
673 if let Some(delete) = targeted
678 && !creating.get(&delete.index_name).accepts_delete()
679 {
680 return Err(DynoxideError::ResourceInUseException(format!(
681 "Attempt to change a resource which is still in use: Index creation is \
682 in resource allocation phase. Retry deletion during backfilling phase \
683 or when the index is active. Table: {} Index: {}",
684 request.table_name, delete.index_name
685 )));
686 }
687
688 for update in updates {
689 if let Some(ref create) = update.create {
690 let target_billing_mode = request
695 .billing_mode
696 .as_deref()
697 .unwrap_or(current_billing_mode);
698 if target_billing_mode != "PAY_PER_REQUEST" {
699 return Err(DynoxideError::ValidationException(
700 "One or more parameter values were invalid: Vector indexes are \
701 only supported for PAY_PER_REQUEST tables"
702 .to_string(),
703 ));
704 }
705
706 if current_vixs
713 .iter()
714 .any(|v| v.index_name == create.index_name)
715 || current_gsis
716 .iter()
717 .any(|g| g.index_name == create.index_name)
718 || lsi_defs.iter().any(|l| l.index_name == create.index_name)
719 {
720 return Err(DynoxideError::ValidationException(
721 "Attempting to create an index which already exists".to_string(),
722 ));
723 }
724
725 validation::validate_vector_index(
730 create,
731 request.attribute_definitions.as_deref().unwrap_or(&[]),
732 "vectorIndexUpdates.1.member.create",
733 )?;
734
735 if current_vixs.len() >= 5 {
741 return Err(DynoxideError::ValidationException(
742 "One or more parameter values were invalid: VectorIndex count \
743 exceeds the per-table limit of 5"
744 .to_string(),
745 ));
746 }
747
748 if current_vixs.iter().any(|v| {
753 v.vector_attribute.attribute_name == create.vector_attribute.attribute_name
754 && v.dimensions != create.dimensions
755 }) {
756 return Err(DynoxideError::ValidationException(format!(
757 "One or more parameter values were invalid: Conflicting attribute \
758 definition for '{}'. All VectorIndexes on the same vector attribute \
759 must use the same dimensions.",
760 create.vector_attribute.attribute_name
761 )));
762 }
763
764 let attr = create.vector_attribute.attribute_name.as_str();
770 if attr_defs.iter().any(|d| d.attribute_name == attr) {
771 return Err(DynoxideError::ValidationException(format!(
772 "One or more parameter values were invalid: Conflicting attribute \
773 definition for '{attr}'. An attribute cannot be defined in \
774 AttributeDefinitions when used as a VectorAttribute."
775 )));
776 }
777 }
778 if let Some(ref delete) = update.delete
779 && !current_vixs
780 .iter()
781 .any(|v| v.index_name == delete.index_name)
782 {
783 return Err(DynoxideError::ResourceNotFoundException(format!(
786 "Requested resource not found: Index {} for table {}",
787 delete.index_name, request.table_name
788 )));
789 }
790 }
791 }
792
793 let now = {
797 use std::sync::atomic::{AtomicU64, Ordering};
798 static LAST_TS: AtomicU64 = AtomicU64::new(0);
799 let wall = web_time::SystemTime::now()
800 .duration_since(web_time::UNIX_EPOCH)
801 .unwrap_or_default()
802 .as_secs_f64();
803 loop {
804 let prev_bits = LAST_TS.load(Ordering::SeqCst);
805 let prev_f = f64::from_bits(prev_bits);
806 let candidate = if wall > prev_f { wall } else { prev_f + 0.001 };
807 let candidate_bits = candidate.to_bits();
808 if LAST_TS
809 .compare_exchange(
810 prev_bits,
811 candidate_bits,
812 Ordering::SeqCst,
813 Ordering::SeqCst,
814 )
815 .is_ok()
816 {
817 break candidate;
818 }
819 }
820 };
821
822 let (cur_rcu, cur_wcu) = parse_current_throughput(&meta);
823 let is_pt_update = request.provisioned_throughput.is_some();
824 let (new_rcu, new_wcu) = if let Some(ref pt) = request.provisioned_throughput {
825 let obj = pt.as_object();
826 (
827 obj.and_then(|o| o.get("ReadCapacityUnits"))
828 .and_then(|v| v.as_i64())
829 .unwrap_or(0),
830 obj.and_then(|o| o.get("WriteCapacityUnits"))
831 .and_then(|v| v.as_i64())
832 .unwrap_or(0),
833 )
834 } else {
835 (cur_rcu, cur_wcu)
836 };
837
838 let is_increase = new_rcu > cur_rcu || new_wcu > cur_wcu;
839 let is_decrease = new_rcu < cur_rcu || new_wcu < cur_wcu;
840
841 let odt_change = request.on_demand_throughput.as_ref().map(|req_odt| {
846 let stored: crate::types::OnDemandThroughput = meta
847 .on_demand_throughput
848 .as_deref()
849 .and_then(|json| serde_json::from_str(json).ok())
850 .unwrap_or_default();
851 let echo = crate::types::OnDemandThroughput {
852 max_read_request_units: req_odt
853 .max_read_request_units
854 .or(stored.max_read_request_units),
855 max_write_request_units: req_odt
856 .max_write_request_units
857 .or(stored.max_write_request_units),
858 };
859 let strip = |v: Option<i64>| v.filter(|&v| v != -1);
860 let effective = crate::types::OnDemandThroughput {
861 max_read_request_units: strip(echo.max_read_request_units),
862 max_write_request_units: strip(echo.max_write_request_units),
863 };
864 (echo, effective)
865 });
866
867 helpers::with_write_transaction(storage, async {
869 if let Some(ref updates) = request.global_secondary_index_updates {
870 for update in updates {
871 if let Some(ref create) = update.create {
872 let gsi_def = GlobalSecondaryIndex {
873 index_name: create.index_name.clone(),
874 key_schema: create.key_schema.clone(),
875 projection: create.projection.clone(),
876 provisioned_throughput: None,
877 };
878
879 storage
880 .create_gsi_table(&request.table_name, &create.index_name)
881 .await?;
882
883 let gsi_p = gsi::gsi_to_def(&gsi_def)?;
884 gsi::backfill_gsi(storage, &request.table_name, &key_schema, &gsi_p).await?;
885
886 current_gsis.push(gsi_def);
887 }
888
889 if let Some(ref delete) = update.delete {
890 storage
891 .drop_gsi_table(&request.table_name, &delete.index_name)
892 .await?;
893 current_gsis.retain(|g| g.index_name != delete.index_name);
894 }
895 }
896 }
897
898 if let Some(ref updates) = request.vector_index_updates {
899 for update in updates {
900 if let Some(ref create) = update.create {
901 storage
902 .create_vector_table(&request.table_name, &create.index_name)
903 .await?;
904
905 backfill_vector_index(
911 storage,
912 &request.table_name,
913 &key_schema,
914 create,
915 &attr_defs,
916 )
917 .await?;
918
919 current_vixs.push(create.clone());
920 }
921
922 if let Some(ref delete) = update.delete {
923 storage
924 .drop_vector_table(&request.table_name, &delete.index_name)
925 .await?;
926 current_vixs.retain(|v| v.index_name != delete.index_name);
927 }
928 }
929
930 super::vector_index::recompute_billed_sizes(
934 storage,
935 &request.table_name,
936 ¤t_vixs,
937 )
938 .await?;
939 }
940
941 reconcile_attribute_definitions(
947 &mut attr_defs,
948 &key_schema,
949 ¤t_gsis,
950 &lsi_defs,
951 ¤t_vixs,
952 );
953
954 let attr_defs_json = serde_json::to_string(&attr_defs)
956 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
957 let gsi_json = if current_gsis.is_empty() {
958 None
959 } else {
960 Some(
961 serde_json::to_string(¤t_gsis)
962 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?,
963 )
964 };
965
966 storage
967 .update_table_metadata(&request.table_name, &attr_defs_json, gsi_json.as_deref())
968 .await?;
969
970 if request
974 .vector_index_updates
975 .as_ref()
976 .is_some_and(|u| !u.is_empty())
977 {
978 let vix_json = if current_vixs.is_empty() {
979 None
980 } else {
981 Some(
982 serde_json::to_string(¤t_vixs)
983 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?,
984 )
985 };
986 storage
987 .update_vector_index_definitions(&request.table_name, vix_json.as_deref())
988 .await?;
989 }
990
991 if is_pt_update {
993 let prev = parse_stored_throughput(&meta);
994 let mut stored = StoredProvisionedThroughput {
995 read_capacity_units: new_rcu,
996 write_capacity_units: new_wcu,
997 last_increase_date_time: prev.as_ref().and_then(|p| p.last_increase_date_time),
998 last_decrease_date_time: prev.as_ref().and_then(|p| p.last_decrease_date_time),
999 number_of_decreases_today: prev
1000 .as_ref()
1001 .and_then(|p| p.number_of_decreases_today)
1002 .or(Some(0)),
1003 };
1004 if is_increase {
1005 stored.last_increase_date_time = Some(now);
1006 }
1007 if is_decrease {
1008 stored.last_decrease_date_time = Some(now);
1009 stored.number_of_decreases_today =
1010 Some(stored.number_of_decreases_today.unwrap_or(0) + 1);
1011 }
1012 let pt_json = serde_json::to_string(&stored)
1013 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
1014 storage
1015 .update_provisioned_throughput(&request.table_name, &pt_json)
1016 .await?;
1017 }
1018
1019 if let Some(enabled) = request.deletion_protection_enabled {
1021 storage
1022 .update_deletion_protection(&request.table_name, enabled)
1023 .await?;
1024 }
1025
1026 if let Some(ref table_class) = request.table_class {
1028 storage
1029 .update_table_class(&request.table_name, table_class)
1030 .await?;
1031 }
1032
1033 if let Some((_, ref effective)) = odt_change {
1035 if effective.max_read_request_units.is_none()
1036 && effective.max_write_request_units.is_none()
1037 {
1038 storage
1039 .clear_on_demand_throughput(&request.table_name)
1040 .await?;
1041 } else {
1042 let json = serde_json::to_string(effective)
1043 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
1044 storage
1045 .update_on_demand_throughput(&request.table_name, &json)
1046 .await?;
1047 }
1048 }
1049
1050 if let Some(ref billing_mode) = request.billing_mode {
1052 storage
1053 .update_billing_mode(&request.table_name, billing_mode)
1054 .await?;
1055 if billing_mode == "PAY_PER_REQUEST" {
1056 storage
1058 .clear_provisioned_throughput(&request.table_name)
1059 .await?;
1060 } else if billing_mode == "PROVISIONED" {
1061 storage
1064 .clear_on_demand_throughput(&request.table_name)
1065 .await?;
1066 }
1067 }
1068
1069 if let Some(ref spec) = request.stream_specification {
1071 if spec.stream_enabled {
1072 let view_type = spec
1073 .stream_view_type
1074 .as_deref()
1075 .unwrap_or("NEW_AND_OLD_IMAGES");
1076 let label = streams::generate_stream_label(storage.clock());
1077 storage
1078 .enable_stream(&request.table_name, view_type, &label)
1079 .await?;
1080 } else {
1081 storage.disable_stream(&request.table_name).await?;
1082 }
1083 }
1084
1085 Ok(())
1086 })
1087 .await?;
1088
1089 if let Some(ref updates) = request.vector_index_updates {
1095 for update in updates {
1096 if let Some(ref create) = update.create {
1097 lifecycle.arm(storage, &request.table_name, &create.index_name);
1098 }
1099 if let Some(ref delete) = update.delete {
1100 lifecycle.disarm(&request.table_name, &delete.index_name);
1101 }
1102 }
1103 }
1104
1105 let updated_meta = helpers::require_table(storage, &request.table_name).await?;
1107 let vector_phases = lifecycle.phases_armed_on(storage, &request.table_name);
1108 let (item_count, table_size_bytes) =
1109 crate::actions::describe_table::item_count_and_size(storage, &request.table_name).await;
1110 let mut desc =
1111 build_table_description(&updated_meta, item_count, table_size_bytes, &vector_phases);
1112 crate::actions::describe_table::fill_index_figures(storage, &mut desc).await;
1113
1114 if let Some((echo, _)) = odt_change {
1117 desc.on_demand_throughput = Some(echo);
1118 }
1119
1120 if let Some(ref updates) = request.vector_index_updates
1128 && updates.iter().any(|u| u.create.is_some())
1129 {
1130 desc.table_status = "UPDATING".to_string();
1131 }
1132
1133 if is_pt_update {
1135 desc.table_status = "UPDATING".to_string();
1136
1137 let stored = parse_stored_throughput(&updated_meta);
1140 if let Some(ref mut pt) = desc.provisioned_throughput {
1141 pt.read_capacity_units = cur_rcu as u64;
1142 pt.write_capacity_units = cur_wcu as u64;
1143 if let Some(ref s) = stored {
1144 pt.last_increase_date_time = s.last_increase_date_time;
1145 pt.last_decrease_date_time = s.last_decrease_date_time;
1146 pt.number_of_decreases_today = s.number_of_decreases_today.unwrap_or(0);
1147 }
1148 }
1149 }
1150
1151 Ok(UpdateTableResponse {
1152 table_description: desc,
1153 })
1154}
1155
1156fn reconcile_attribute_definitions(
1168 attr_defs: &mut Vec<AttributeDefinition>,
1169 key_schema: &helpers::KeySchema,
1170 gsis: &[GlobalSecondaryIndex],
1171 lsi_defs: &[crate::actions::lsi::LsiDef],
1172 vixs: &[VectorIndex],
1173) {
1174 let mut used: std::collections::HashSet<&str> = std::collections::HashSet::new();
1175 used.insert(key_schema.partition_key.as_str());
1176 if let Some(ref sk) = key_schema.sort_key {
1177 used.insert(sk.as_str());
1178 }
1179 for g in gsis {
1180 for k in &g.key_schema {
1181 used.insert(k.attribute_name.as_str());
1182 }
1183 }
1184 for lsi in lsi_defs {
1185 used.insert(lsi.pk_attr.as_str());
1186 if let Some(ref sk) = lsi.sk_attr {
1187 used.insert(sk.as_str());
1188 }
1189 }
1190 for vix in vixs {
1191 if let Some(ref schema) = vix.search_schema {
1192 for elem in schema {
1193 used.insert(elem.attribute_name.as_str());
1194 }
1195 }
1196 }
1197 attr_defs.retain(|d| used.contains(d.attribute_name.as_str()));
1198}
1199
1200fn collect_gsi_update_errors(updates: &[GlobalSecondaryIndexUpdate], errors: &mut Vec<String>) {
1204 for (i, update) in updates.iter().enumerate() {
1205 if let Some(ref upd) = update.update {
1206 if upd.index_name.len() < 3 {
1208 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));
1209 }
1210 if !upd.index_name.is_empty()
1211 && !upd
1212 .index_name
1213 .chars()
1214 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
1215 {
1216 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));
1217 }
1218 if let Some(ref pt) = upd.provisioned_throughput {
1220 let wcu = pt.write_capacity_units;
1221 let rcu = pt.read_capacity_units;
1222 if wcu.is_none() {
1223 errors.push(format!("Value null at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
1224 } else if let Some(w) = wcu
1225 && w < 1
1226 {
1227 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));
1228 }
1229 if rcu.is_none() {
1230 errors.push(format!("Value null at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
1231 } else if let Some(r) = rcu
1232 && r < 1
1233 {
1234 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));
1235 }
1236 } else {
1237 errors.push(format!("Value null at 'globalSecondaryIndexUpdates.{}.member.update.provisionedThroughput' failed to satisfy constraint: Member must not be null", i + 1));
1238 }
1239 }
1240 }
1241}
1242
1243fn validate_update_request(request: &UpdateTableRequest) -> Result<()> {
1248 let mut errors = Vec::new();
1250
1251 if let Some(ref pt) = request.provisioned_throughput
1253 && let Some(obj) = pt.as_object()
1254 {
1255 let wcu = obj.get("WriteCapacityUnits");
1256 let rcu = obj.get("ReadCapacityUnits");
1257 if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
1258 errors.push("Value null at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
1259 } else if let Some(w) = wcu.and_then(|v| v.as_i64())
1260 && w < 1
1261 {
1262 errors.push(format!("Value '{}' at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w));
1263 }
1264 if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
1265 errors.push("Value null at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
1266 } else if let Some(r) = rcu.and_then(|v| v.as_i64())
1267 && r < 1
1268 {
1269 errors.push(format!("Value '{}' at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r));
1270 }
1271 }
1272
1273 if let Some(ref updates) = request.global_secondary_index_updates {
1275 collect_gsi_update_errors(updates, &mut errors);
1276 }
1277
1278 errors.truncate(10);
1280
1281 if !errors.is_empty() {
1282 let prefix = format!(
1283 "{} validation error{} detected: ",
1284 errors.len(),
1285 if errors.len() == 1 { "" } else { "s" }
1286 );
1287 return Err(DynoxideError::ValidationException(format!(
1288 "{}{}",
1289 prefix,
1290 errors.join("; ")
1291 )));
1292 }
1293
1294 if let Some(ref bm) = request.billing_mode
1298 && bm != "PROVISIONED"
1299 && bm != "PAY_PER_REQUEST"
1300 {
1301 return Err(DynoxideError::ValidationException(format!(
1302 "1 validation error detected: Value '{}' at 'billingMode' \
1303 failed to satisfy constraint: Member must satisfy enum value set: \
1304 [PROVISIONED, PAY_PER_REQUEST]",
1305 bm
1306 )));
1307 }
1308
1309 if let Some(ref tc) = request.table_class
1311 && tc != "STANDARD"
1312 && tc != "STANDARD_INFREQUENT_ACCESS"
1313 {
1314 return Err(DynoxideError::ValidationException(format!(
1315 "1 validation error detected: Value '{tc}' at 'tableClass' failed to satisfy \
1316 constraint: Member must satisfy enum value set: \
1317 [STANDARD, STANDARD_INFREQUENT_ACCESS]"
1318 )));
1319 }
1320
1321 if request.billing_mode.as_deref() == Some("PAY_PER_REQUEST")
1323 && request.provisioned_throughput.is_some()
1324 {
1325 return Err(DynoxideError::ValidationException(
1326 "One or more parameter values were invalid: \
1327 Neither ReadCapacityUnits nor WriteCapacityUnits can be \
1328 specified when BillingMode is PAY_PER_REQUEST"
1329 .to_string(),
1330 ));
1331 }
1332
1333 if let Some(ref pt) = request.provisioned_throughput
1335 && let Some(obj) = pt.as_object()
1336 {
1337 let rcu = obj
1338 .get("ReadCapacityUnits")
1339 .and_then(|v| v.as_i64())
1340 .unwrap_or(0);
1341 let wcu = obj
1342 .get("WriteCapacityUnits")
1343 .and_then(|v| v.as_i64())
1344 .unwrap_or(0);
1345 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
1346 if rcu > MAX_THROUGHPUT {
1347 return Err(DynoxideError::ValidationException(format!(
1348 "Given value {} for ReadCapacityUnits is out of bounds",
1349 rcu
1350 )));
1351 }
1352 if wcu > MAX_THROUGHPUT {
1353 return Err(DynoxideError::ValidationException(format!(
1354 "Given value {} for WriteCapacityUnits is out of bounds",
1355 wcu
1356 )));
1357 }
1358 }
1359
1360 let no_config_change = request.provisioned_throughput.is_none()
1366 && request.billing_mode.is_none()
1367 && request.stream_specification.is_none()
1368 && request.deletion_protection_enabled.is_none()
1369 && request.table_class.is_none()
1370 && request.on_demand_throughput.is_none();
1371 let no_gsi_change = request
1372 .global_secondary_index_updates
1373 .as_ref()
1374 .is_none_or(|updates| updates.is_empty());
1375 let no_vector_change = request
1376 .vector_index_updates
1377 .as_ref()
1378 .is_none_or(|updates| updates.is_empty());
1379 if no_gsi_change && no_config_change && no_vector_change {
1380 return Err(DynoxideError::ValidationException(
1381 "At least one of ProvisionedThroughput, BillingMode, UpdateStreamEnabled, GlobalSecondaryIndexUpdates or SSESpecification or ReplicaUpdates is required".to_string(),
1382 ));
1383 }
1384
1385 if let Some(ref updates) = request.global_secondary_index_updates {
1387 for update in updates {
1389 if update.update.is_none() && update.create.is_none() && update.delete.is_none() {
1390 return Err(DynoxideError::ValidationException(
1391 "One or more parameter values were invalid: One of GlobalSecondaryIndexUpdate.Update, GlobalSecondaryIndexUpdate.Create, GlobalSecondaryIndexUpdate.Delete must not be null".to_string(),
1392 ));
1393 }
1394 }
1395
1396 let mut seen_names = std::collections::HashSet::new();
1398 for update in updates {
1399 let name = if let Some(ref u) = update.update {
1400 Some(u.index_name.as_str())
1401 } else if let Some(ref c) = update.create {
1402 Some(c.index_name.as_str())
1403 } else {
1404 update.delete.as_ref().map(|d| d.index_name.as_str())
1405 };
1406 if let Some(name) = name
1407 && !seen_names.insert(name.to_string())
1408 {
1409 return Err(DynoxideError::ValidationException(format!(
1410 "One or more parameter values were invalid: Only one global secondary index update per index is allowed simultaneously. Index: {}",
1411 name
1412 )));
1413 }
1414 }
1415 }
1416
1417 if let Some(ref updates) = request.vector_index_updates {
1421 for update in updates {
1422 if update.create.is_none() && update.delete.is_none() {
1423 return Err(DynoxideError::ValidationException(
1424 "One or more parameter values were invalid: One of \
1425 VectorIndexUpdate.Create, VectorIndexUpdate.Delete must not be null"
1426 .to_string(),
1427 ));
1428 }
1429 if update.create.is_some() && update.delete.is_some() {
1434 return Err(DynoxideError::ValidationException(
1435 "One or more parameter values were invalid: Only one vector index \
1436 action is allowed per VectorIndexUpdate object"
1437 .to_string(),
1438 ));
1439 }
1440 }
1441 }
1442
1443 Ok(())
1444}
1445
1446#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1448struct StoredProvisionedThroughput {
1449 #[serde(rename = "ReadCapacityUnits")]
1450 read_capacity_units: i64,
1451 #[serde(rename = "WriteCapacityUnits")]
1452 write_capacity_units: i64,
1453 #[serde(
1454 rename = "LastIncreaseDateTime",
1455 skip_serializing_if = "Option::is_none"
1456 )]
1457 last_increase_date_time: Option<f64>,
1458 #[serde(
1459 rename = "LastDecreaseDateTime",
1460 skip_serializing_if = "Option::is_none"
1461 )]
1462 last_decrease_date_time: Option<f64>,
1463 #[serde(
1464 rename = "NumberOfDecreasesToday",
1465 skip_serializing_if = "Option::is_none"
1466 )]
1467 number_of_decreases_today: Option<u64>,
1468}
1469
1470fn parse_current_throughput(meta: &crate::storage::TableMetadata) -> (i64, i64) {
1472 parse_stored_throughput(meta)
1473 .map(|pt| (pt.read_capacity_units, pt.write_capacity_units))
1474 .unwrap_or((0, 0))
1475}
1476
1477fn parse_stored_throughput(
1479 meta: &crate::storage::TableMetadata,
1480) -> Option<StoredProvisionedThroughput> {
1481 meta.provisioned_throughput
1482 .as_ref()
1483 .and_then(|pt_json| serde_json::from_str(pt_json).ok())
1484}
1485
1486async fn backfill_vector_index<S: StorageBackend>(
1494 storage: &S,
1495 table_name: &str,
1496 key_schema: &helpers::KeySchema,
1497 vix: &VectorIndex,
1498 attr_defs: &[AttributeDefinition],
1499) -> Result<()> {
1500 const BATCH_SIZE: usize = 1000;
1501 let mut last_pk: Option<String> = None;
1502 let mut last_sk: Option<String> = None;
1503
1504 loop {
1505 let items = storage
1506 .scan_items(
1507 table_name,
1508 &crate::storage::ScanParams {
1509 limit: Some(BATCH_SIZE),
1510 exclusive_start_pk: last_pk.as_deref(),
1511 exclusive_start_sk: last_sk.as_deref(),
1512 ..Default::default()
1513 },
1514 )
1515 .await?;
1516
1517 if items.is_empty() {
1518 break;
1519 }
1520
1521 let mut rows: Vec<crate::storage_backend::VectorItemRow> = Vec::new();
1522 for (pk, sk, item_json) in &items {
1523 let item: Item = serde_json::from_str(item_json)
1524 .map_err(|e| DynoxideError::InternalServerError(format!("Bad item JSON: {e}")))?;
1525
1526 if let Some(row) = super::vector_index::vector_index_row(
1527 &item,
1528 vix,
1529 &key_schema.partition_key,
1530 key_schema.sort_key.as_deref(),
1531 attr_defs,
1532 pk,
1533 sk,
1534 )? {
1535 rows.push(row);
1536 }
1537 }
1538
1539 storage
1540 .insert_vector_items(table_name, &vix.index_name, &rows)
1541 .await?;
1542
1543 let last = &items[items.len() - 1];
1544 last_pk = Some(last.0.clone());
1545 last_sk = Some(last.1.clone());
1546
1547 if items.len() < BATCH_SIZE {
1548 break;
1549 }
1550 }
1551
1552 Ok(())
1553}