1use crate::actions::{TableDescription, build_table_description};
2use crate::errors::{DynoxideError, Result};
3use crate::storage_backend::{BackendError, StorageBackend};
4use crate::streams;
5use crate::types::{
6 AttributeDefinition, GlobalSecondaryIndex, KeySchemaElement, KeyType, LocalSecondaryIndex,
7 Projection, ProjectionType, ProvisionedThroughput, VectorIndex,
8};
9use serde::{Deserialize, Serialize};
10use web_time::{SystemTime, UNIX_EPOCH};
11
12#[derive(Debug, Default, Deserialize)]
15struct RawRequest {
16 #[serde(rename = "TableName", default)]
17 table_name: Option<String>,
18 #[serde(rename = "KeySchema", default)]
19 key_schema: Option<serde_json::Value>,
20 #[serde(rename = "AttributeDefinitions", default)]
21 attribute_definitions: Option<serde_json::Value>,
22 #[serde(rename = "GlobalSecondaryIndexes", default)]
23 global_secondary_indexes: Option<serde_json::Value>,
24 #[serde(rename = "LocalSecondaryIndexes", default)]
25 local_secondary_indexes: Option<serde_json::Value>,
26 #[serde(rename = "VectorIndexes", default)]
27 vector_indexes: Option<serde_json::Value>,
28 #[serde(rename = "BillingMode", default)]
29 billing_mode: Option<String>,
30 #[serde(rename = "ProvisionedThroughput", default)]
31 provisioned_throughput: Option<serde_json::Value>,
32 #[serde(rename = "StreamSpecification", default)]
33 stream_specification: Option<StreamSpecification>,
34 #[serde(rename = "SSESpecification", default)]
35 sse_specification: Option<crate::types::SseSpecification>,
36 #[serde(rename = "TableClass", default)]
37 table_class: Option<String>,
38 #[serde(rename = "Tags", default)]
39 tags: Option<Vec<crate::types::Tag>>,
40 #[serde(rename = "DeletionProtectionEnabled", default)]
41 deletion_protection_enabled: Option<bool>,
42 #[serde(rename = "OnDemandThroughput", default)]
43 on_demand_throughput: Option<crate::types::OnDemandThroughput>,
44}
45
46#[derive(Debug, Default)]
49pub struct CreateTableRequest {
50 pub table_name: String,
51 pub key_schema: Vec<KeySchemaElement>,
52 pub attribute_definitions: Vec<AttributeDefinition>,
53 pub global_secondary_indexes: Option<Vec<GlobalSecondaryIndex>>,
54 pub local_secondary_indexes: Option<Vec<LocalSecondaryIndex>>,
55 pub vector_indexes: Option<Vec<VectorIndex>>,
56 pub billing_mode: Option<String>,
57 pub provisioned_throughput: Option<ProvisionedThroughput>,
58 pub stream_specification: Option<StreamSpecification>,
59 pub sse_specification: Option<crate::types::SseSpecification>,
60 pub table_class: Option<String>,
61 pub tags: Option<Vec<crate::types::Tag>>,
62 pub deletion_protection_enabled: Option<bool>,
63 pub on_demand_throughput: Option<crate::types::OnDemandThroughput>,
64}
65
66impl<'de> serde::Deserialize<'de> for CreateTableRequest {
69 fn deserialize<D: serde::Deserializer<'de>>(
70 deserializer: D,
71 ) -> std::result::Result<Self, D::Error> {
72 let raw = RawRequest::deserialize(deserializer)?;
73 match validate_raw_and_build(raw) {
74 Ok(req) => Ok(req),
75 Err(msg) => Err(serde::de::Error::custom(format!("VALIDATION:{}", msg))),
76 }
77 }
78}
79
80#[derive(Debug, Default, Deserialize)]
81pub struct StreamSpecification {
82 #[serde(rename = "StreamEnabled", alias = "stream_enabled")]
83 pub stream_enabled: bool,
84 #[serde(rename = "StreamViewType", alias = "stream_view_type", default)]
85 pub stream_view_type: Option<String>,
86}
87
88#[derive(Debug, Default, Serialize)]
89pub struct CreateTableResponse {
90 #[serde(rename = "TableDescription")]
91 pub table_description: TableDescription,
92}
93
94pub async fn execute<S: StorageBackend>(
95 storage: &S,
96 mut request: CreateTableRequest,
97) -> Result<CreateTableResponse> {
98 if request.on_demand_throughput.as_ref().is_some_and(|odt| {
102 odt.max_read_request_units.is_none() && odt.max_write_request_units.is_none()
103 }) {
104 request.on_demand_throughput = None;
105 }
106
107 validate_typed_request(&request)?;
109
110 if let Some(ref bm) = request.billing_mode
111 && bm != "PROVISIONED"
112 && bm != "PAY_PER_REQUEST"
113 {
114 return Err(DynoxideError::ValidationException(format!(
115 "1 validation error detected: Value '{bm}' at 'billingMode' failed to satisfy \
116 constraint: Member must satisfy enum value set: \
117 [PROVISIONED, PAY_PER_REQUEST]"
118 )));
119 }
120
121 if let Some(ref tc) = request.table_class
122 && tc != "STANDARD"
123 && tc != "STANDARD_INFREQUENT_ACCESS"
124 {
125 return Err(DynoxideError::ValidationException(format!(
126 "1 validation error detected: Value '{tc}' at 'tableClass' failed to satisfy \
127 constraint: Member must satisfy enum value set: \
128 [STANDARD, STANDARD_INFREQUENT_ACCESS]"
129 )));
130 }
131
132 if let Some(ref odt) = request.on_demand_throughput {
138 let members = [
139 ("MaxReadRequestUnits", odt.max_read_request_units),
140 ("MaxWriteRequestUnits", odt.max_write_request_units),
141 ];
142 if let Some((member, _)) = members.iter().find(|(_, v)| v.is_some()) {
143 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
144 if billing_mode_str == "PROVISIONED" {
145 return Err(DynoxideError::ValidationException(format!(
146 "One or more parameter values were invalid: {member} for \
147 OnDemandThroughput cannot be specified when table BillingMode \
148 is PROVISIONED."
149 )));
150 }
151 }
152 for (member, value) in members {
153 if value.is_some_and(|v| v < 1) {
154 return Err(DynoxideError::ValidationException(format!(
155 "One or more parameter values were invalid: Requested {member} for \
156 OnDemandThroughput for table is outside of valid range"
157 )));
158 }
159 }
160 }
161
162 if storage.table_exists(&request.table_name).await? {
163 return Err(DynoxideError::ResourceInUseException(format!(
164 "Table already exists: {}",
165 request.table_name
166 )));
167 }
168
169 let now = SystemTime::now()
170 .duration_since(UNIX_EPOCH)
171 .unwrap_or_default()
172 .as_secs() as i64;
173
174 let key_schema_json = serde_json::to_string(&request.key_schema)
175 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
176 let attr_defs_json = serde_json::to_string(&request.attribute_definitions)
177 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
178 let gsi_json = request
179 .global_secondary_indexes
180 .as_ref()
181 .map(serde_json::to_string)
182 .transpose()
183 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
184 let lsi_json = request
185 .local_secondary_indexes
186 .as_ref()
187 .map(serde_json::to_string)
188 .transpose()
189 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
190 let vector_json = request
191 .vector_indexes
192 .as_ref()
193 .map(serde_json::to_string)
194 .transpose()
195 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
196 let pt_json = request
197 .provisioned_throughput
198 .as_ref()
199 .map(serde_json::to_string)
200 .transpose()
201 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
202 let normalized_sse = request.sse_specification.as_ref().map(|spec| {
207 if spec.enabled == Some(true) {
208 crate::types::SseSpecification {
209 enabled: Some(true),
210 sse_type: spec.sse_type.clone().or_else(|| Some("KMS".to_string())),
211 kms_master_key_id: spec.kms_master_key_id.clone().or_else(|| {
212 Some(crate::streams::kms_key_arn(
213 &uuid::Uuid::new_v4().to_string(),
214 ))
215 }),
216 }
217 } else {
218 spec.clone()
219 }
220 });
221 let sse_json = normalized_sse
222 .as_ref()
223 .map(serde_json::to_string)
224 .transpose()
225 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
226 let on_demand_json = request
227 .on_demand_throughput
228 .as_ref()
229 .map(serde_json::to_string)
230 .transpose()
231 .map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
232 let deletion_protection = request.deletion_protection_enabled.unwrap_or(false);
233
234 if let Some(ref spec) = request.stream_specification
239 && spec.stream_enabled
240 && !storage.supports_streams()
241 {
242 return Err(BackendError::Unsupported {
243 capability: "streams",
244 }
245 .into());
246 }
247 if request.tags.as_ref().is_some_and(|tags| !tags.is_empty()) && !storage.supports_tags() {
248 return Err(BackendError::Unsupported { capability: "tags" }.into());
249 }
250
251 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
252 storage
253 .insert_table_metadata(&crate::storage::CreateTableMetadata {
254 table_name: &request.table_name,
255 key_schema: &key_schema_json,
256 attribute_definitions: &attr_defs_json,
257 gsi_definitions: gsi_json.as_deref(),
258 lsi_definitions: lsi_json.as_deref(),
259 provisioned_throughput: pt_json.as_deref(),
260 created_at: now,
261 sse_specification: sse_json.as_deref(),
262 table_class: request.table_class.as_deref(),
263 deletion_protection_enabled: deletion_protection,
264 billing_mode: Some(billing_mode_str),
265 on_demand_throughput: on_demand_json.as_deref(),
266 vector_index_definitions: vector_json.as_deref(),
267 })
268 .await?;
269
270 storage.create_data_table(&request.table_name).await?;
271
272 if let Some(ref gsis) = request.global_secondary_indexes {
273 for gsi in gsis {
274 storage
275 .create_gsi_table(&request.table_name, &gsi.index_name)
276 .await?;
277 }
278 }
279
280 if let Some(ref lsis) = request.local_secondary_indexes {
281 for lsi in lsis {
282 storage
283 .create_lsi_table(&request.table_name, &lsi.index_name)
284 .await?;
285 }
286 }
287
288 if let Some(ref vixs) = request.vector_indexes {
289 for vix in vixs {
290 storage
291 .create_vector_table(&request.table_name, &vix.index_name)
292 .await?;
293 }
294 }
295
296 if let Some(ref spec) = request.stream_specification
297 && spec.stream_enabled
298 {
299 let view_type = spec
300 .stream_view_type
301 .as_deref()
302 .unwrap_or("NEW_AND_OLD_IMAGES");
303 let label = streams::generate_stream_label(storage.clock());
304 storage
305 .enable_stream(&request.table_name, view_type, &label)
306 .await?;
307 }
308
309 if let Some(ref tags) = request.tags
310 && !tags.is_empty()
311 {
312 storage.set_tags(&request.table_name, tags).await?;
313 }
314
315 let meta = storage
316 .get_table_metadata(&request.table_name)
317 .await?
318 .ok_or_else(|| {
319 DynoxideError::InternalServerError("Table metadata not found after creation".into())
320 })?;
321
322 let mut desc = build_table_description(
326 &meta,
327 Some(0),
328 Some(0),
329 &crate::actions::vector_lifecycle::VectorIndexPhases::default(),
330 );
331 desc.table_status = "CREATING".to_string();
334
335 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
337 if billing_mode_str == "PROVISIONED" {
338 desc.billing_mode_summary = None;
339 desc.table_throughput_mode_summary = None;
340 } else if billing_mode_str == "PAY_PER_REQUEST" {
341 desc.billing_mode_summary = Some(crate::actions::BillingModeSummary {
342 billing_mode: "PAY_PER_REQUEST".to_string(),
343 last_update_to_pay_per_request_date_time: None,
344 });
345 desc.table_throughput_mode_summary = Some(crate::actions::TableThroughputModeSummary {
346 table_throughput_mode: "PAY_PER_REQUEST".to_string(),
347 last_update_to_pay_per_request_date_time: None,
348 });
349 desc.provisioned_throughput = Some(crate::actions::TableProvisionedThroughputDescription {
351 read_capacity_units: 0,
352 write_capacity_units: 0,
353 number_of_decreases_today: 0,
354 last_increase_date_time: None,
355 last_decrease_date_time: None,
356 });
357 }
358
359 if let Some(ref mut gsis) = desc.global_secondary_indexes {
361 for gsi in gsis {
362 gsi.index_status = "CREATING".to_string();
363 }
364 }
365
366 if let Some(ref mut vixs) = desc.vector_indexes {
370 for vix in vixs {
371 vix.index_status = "CREATING".to_string();
372 }
373 }
374
375 if request.deletion_protection_enabled.is_none() {
378 desc.deletion_protection_enabled = None;
379 }
380
381 Ok(CreateTableResponse {
382 table_description: desc,
383 })
384}
385
386fn ve(msg: String) -> DynoxideError {
388 DynoxideError::ValidationException(msg)
389}
390
391fn validate_typed_request(request: &CreateTableRequest) -> Result<()> {
411 if request.table_name.is_empty() {
412 return Err(DynoxideError::ValidationException(
413 "The parameter 'TableName' is required but was not present in the request".to_string(),
414 ));
415 }
416 if request.table_name.len() < 3 || request.table_name.len() > 255 {
417 return Err(DynoxideError::ValidationException(
418 "TableName must be at least 3 characters long and at most 255 characters long"
419 .to_string(),
420 ));
421 }
422
423 if !request
425 .table_name
426 .chars()
427 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
428 {
429 return Err(DynoxideError::ValidationException(format!(
430 "1 validation error detected: Value '{}' at 'tableName' failed to satisfy constraint: \
431 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
432 request.table_name
433 )));
434 }
435
436 let billing_mode_str = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
438 if billing_mode_str == "PAY_PER_REQUEST" && request.provisioned_throughput.is_some() {
439 return Err(DynoxideError::ValidationException(
440 "One or more parameter values were invalid: Neither ReadCapacityUnits nor \
441 WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"
442 .to_string(),
443 ));
444 }
445
446 if let Some(ref pt) = request.provisioned_throughput {
448 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
449 let rcu = pt.read_capacity_units.unwrap_or(0);
450 let wcu = pt.write_capacity_units.unwrap_or(0);
451 if rcu > MAX_THROUGHPUT {
452 return Err(DynoxideError::ValidationException(format!(
453 "Given value {} for ReadCapacityUnits is out of bounds",
454 rcu
455 )));
456 }
457 if wcu > MAX_THROUGHPUT {
458 return Err(DynoxideError::ValidationException(format!(
459 "Given value {} for WriteCapacityUnits is out of bounds",
460 wcu
461 )));
462 }
463 }
464
465 if request.billing_mode.is_some()
470 && billing_mode_str == "PROVISIONED"
471 && request.provisioned_throughput.is_none()
472 {
473 return Err(DynoxideError::ValidationException(
474 "One or more parameter values were invalid: ReadCapacityUnits and \
475 WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
476 .to_string(),
477 ));
478 }
479
480 validate_key_attrs_in_defs(&request.key_schema, &request.attribute_definitions).map_err(ve)?;
482
483 validate_key_schema_structure(&request.key_schema).map_err(ve)?;
485
486 if let Some(ref lsis) = request.local_secondary_indexes
488 && lsis.is_empty()
489 {
490 return Err(ve(
491 "One or more parameter values were invalid: List of LocalSecondaryIndexes is empty"
492 .to_string(),
493 ));
494 }
495 if let Some(ref gsis) = request.global_secondary_indexes
496 && gsis.is_empty()
497 {
498 return Err(ve(
499 "One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty"
500 .to_string(),
501 ));
502 }
503 if let Some(ref vixs) = request.vector_indexes
506 && vixs.is_empty()
507 {
508 return Err(ve(
509 "One or more parameter values were invalid: List of VectorIndexes is empty".to_string(),
510 ));
511 }
512
513 if let Some(ref lsis) = request.local_secondary_indexes {
515 validate_lsi_list(lsis, &request.key_schema, &request.attribute_definitions).map_err(ve)?;
516 }
517
518 if let Some(ref gsis) = request.global_secondary_indexes {
520 let bm = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
521 validate_gsi_list(gsis, &request.attribute_definitions, bm).map_err(ve)?;
522 }
523
524 if let Some(ref vixs) = request.vector_indexes {
526 let bm = request.billing_mode.as_deref().unwrap_or("PROVISIONED");
527 validate_vector_index_list(vixs, &request.attribute_definitions, bm)?;
528 }
529
530 check_cross_index_duplicates(
532 &request.local_secondary_indexes,
533 &request.global_secondary_indexes,
534 &request.vector_indexes,
535 )
536 .map_err(ve)?;
537
538 validate_attr_def_count(
542 &request.key_schema,
543 &request.attribute_definitions,
544 &request.local_secondary_indexes,
545 &request.global_secondary_indexes,
546 &request.vector_indexes,
547 )
548 .map_err(ve)?;
549
550 if let Some(ref spec) = request.stream_specification
554 && !spec.stream_enabled
555 && spec.stream_view_type.is_some()
556 {
557 return Err(DynoxideError::ValidationException(
558 "One or more parameter values were invalid: Table is being created with a stream \
559 disabled, UpdateViewType should not be specified"
560 .to_string(),
561 ));
562 }
563
564 Ok(())
565}
566
567fn validate_vector_index_list(
579 vixs: &[VectorIndex],
580 defs: &[AttributeDefinition],
581 bm: &str,
582) -> Result<()> {
583 if bm != "PAY_PER_REQUEST" {
584 return Err(ve(
585 "One or more parameter values were invalid: Vector indexes are only supported \
586 for PAY_PER_REQUEST tables"
587 .to_string(),
588 ));
589 }
590
591 for (i, vix) in vixs.iter().enumerate() {
592 crate::validation::validate_vector_index(
593 vix,
594 defs,
595 &format!("vectorIndexes.{}.member", i + 1),
596 )?;
597 }
598
599 let mut seen = std::collections::HashSet::new();
601 for vix in vixs {
602 if !seen.insert(&vix.index_name) {
603 return Err(ve(format!(
604 "One or more parameter values were invalid: Duplicate index name: {}",
605 vix.index_name
606 )));
607 }
608 }
609
610 if vixs.len() > 5 {
612 return Err(ve(
613 "One or more parameter values were invalid: VectorIndex count exceeds the \
614 per-table limit of 5"
615 .to_string(),
616 ));
617 }
618
619 let mut dims_by_attr: std::collections::HashMap<&str, u32> = std::collections::HashMap::new();
621 for vix in vixs {
622 let attr = vix.vector_attribute.attribute_name.as_str();
623 match dims_by_attr.get(attr) {
624 Some(&prev) if prev != vix.dimensions => {
625 return Err(ve(format!(
626 "One or more parameter values were invalid: Conflicting attribute \
627 definition for '{attr}'. All VectorIndexes on the same vector attribute \
628 must use the same dimensions."
629 )));
630 }
631 Some(_) => {}
632 None => {
633 dims_by_attr.insert(attr, vix.dimensions);
634 }
635 }
636 }
637
638 for vix in vixs {
642 let attr = vix.vector_attribute.attribute_name.as_str();
643 if defs.iter().any(|d| d.attribute_name == attr) {
644 return Err(ve(format!(
645 "One or more parameter values were invalid: Conflicting attribute definition \
646 for '{attr}'. An attribute cannot be defined in AttributeDefinitions when \
647 used as a VectorAttribute."
648 )));
649 }
650 }
651
652 Ok(())
653}
654
655fn check_cross_index_duplicates(
662 lsis: &Option<Vec<LocalSecondaryIndex>>,
663 gsis: &Option<Vec<GlobalSecondaryIndex>>,
664 vixs: &Option<Vec<VectorIndex>>,
665) -> std::result::Result<(), String> {
666 let mut all_names = std::collections::HashSet::new();
667 if let Some(lsis) = lsis {
668 for lsi in lsis {
669 all_names.insert(&lsi.index_name);
670 }
671 }
672 if let Some(gsis) = gsis {
673 for gsi in gsis {
674 if !all_names.insert(&gsi.index_name) {
675 return Err(format!(
676 "One or more parameter values were invalid: Duplicate index name: {}",
677 gsi.index_name
678 ));
679 }
680 }
681 }
682 if let Some(vixs) = vixs {
683 for vix in vixs {
684 if !all_names.insert(&vix.index_name) {
685 return Err(format!(
686 "One or more parameter values were invalid: Duplicate index name: {}",
687 vix.index_name
688 ));
689 }
690 }
691 }
692 Ok(())
693}
694
695fn validate_raw_and_build(raw: RawRequest) -> std::result::Result<CreateTableRequest, String> {
698 if raw.table_name.is_none() {
700 return Err(
701 "The parameter 'TableName' is required but was not present in the request".to_string(),
702 );
703 }
704
705 let name_errors = crate::validation::table_name_constraint_errors(
709 raw.table_name.as_deref(),
710 crate::validation::TableNameContext::CreateTable,
711 );
712 if !name_errors.is_empty() {
713 let msg = format!(
714 "{} validation error{} detected: {}",
715 name_errors.len(),
716 if name_errors.len() > 1 { "s" } else { "" },
717 name_errors.join("; ")
718 );
719 return Err(msg);
720 }
721 let table_name = raw.table_name.unwrap();
722
723 let mut errors = Vec::new();
724
725 if let Some(ref bm) = raw.billing_mode
726 && bm != "PROVISIONED"
727 && bm != "PAY_PER_REQUEST"
728 {
729 errors.push(format!(
730 "Value '{}' at 'billingMode' failed to satisfy constraint: \
731 Member must satisfy enum value set: [PROVISIONED, PAY_PER_REQUEST]",
732 bm
733 ));
734 }
735
736 collect_pt_errors(&raw.provisioned_throughput, &mut errors);
737 collect_ks_errors(&raw.key_schema, &mut errors);
738 collect_ad_errors(&raw.attribute_definitions, &mut errors);
739 collect_lsi_errors(&raw.local_secondary_indexes, &mut errors);
740 collect_gsi_errors(&raw.global_secondary_indexes, &mut errors);
741 collect_vix_errors(&raw.vector_indexes, &mut errors);
742
743 errors.truncate(10);
745
746 if !errors.is_empty() {
747 let prefix = format!(
748 "{} validation error{} detected: ",
749 errors.len(),
750 if errors.len() == 1 { "" } else { "s" }
751 );
752 return Err(format!("{}{}", prefix, errors.join("; ")));
753 }
754
755 let billing_mode_str = raw.billing_mode.as_deref().unwrap_or("PROVISIONED");
757 if billing_mode_str == "PAY_PER_REQUEST" && raw.provisioned_throughput.is_some() {
758 return Err(
759 "One or more parameter values were invalid: Neither ReadCapacityUnits nor \
760 WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"
761 .to_string(),
762 );
763 }
764
765 if let Some(ref pt) = raw.provisioned_throughput
767 && let Some(obj) = pt.as_object()
768 {
769 let rcu = obj
770 .get("ReadCapacityUnits")
771 .and_then(|v| v.as_i64())
772 .unwrap_or(0);
773 let wcu = obj
774 .get("WriteCapacityUnits")
775 .and_then(|v| v.as_i64())
776 .unwrap_or(0);
777 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
778 if rcu > MAX_THROUGHPUT {
779 return Err(format!(
780 "Given value {} for ReadCapacityUnits is out of bounds",
781 rcu
782 ));
783 }
784 if wcu > MAX_THROUGHPUT {
785 return Err(format!(
786 "Given value {} for WriteCapacityUnits is out of bounds",
787 wcu
788 ));
789 }
790 }
791
792 if raw.billing_mode.as_deref() == Some("PROVISIONED") && raw.provisioned_throughput.is_none() {
794 return Err(
795 "One or more parameter values were invalid: ReadCapacityUnits and \
796 WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
797 .to_string(),
798 );
799 }
800
801 let key_schema: Vec<KeySchemaElement> = raw
803 .key_schema
804 .as_ref()
805 .map(|v| serde_json::from_value(v.clone()))
806 .transpose()
807 .map_err(|e| e.to_string())?
808 .unwrap_or_default();
809 let attribute_definitions: Vec<AttributeDefinition> = raw
810 .attribute_definitions
811 .as_ref()
812 .map(|v| serde_json::from_value(v.clone()))
813 .transpose()
814 .map_err(|e| e.to_string())?
815 .unwrap_or_default();
816 let provisioned_throughput: Option<ProvisionedThroughput> = raw
817 .provisioned_throughput
818 .as_ref()
819 .map(|v| serde_json::from_value(v.clone()))
820 .transpose()
821 .map_err(|e| e.to_string())?;
822 let global_secondary_indexes: Option<Vec<GlobalSecondaryIndex>> = raw
823 .global_secondary_indexes
824 .as_ref()
825 .map(|v| serde_json::from_value(v.clone()))
826 .transpose()
827 .map_err(|e| e.to_string())?;
828 let local_secondary_indexes: Option<Vec<LocalSecondaryIndex>> = raw
829 .local_secondary_indexes
830 .as_ref()
831 .map(|v| serde_json::from_value(v.clone()))
832 .transpose()
833 .map_err(|e| e.to_string())?;
834 let vector_indexes: Option<Vec<VectorIndex>> = raw
835 .vector_indexes
836 .as_ref()
837 .map(|v| serde_json::from_value(normalise_vix_dimensions(v)))
838 .transpose()
839 .map_err(|e| e.to_string())?;
840
841 Ok(CreateTableRequest {
842 table_name,
843 key_schema,
844 attribute_definitions,
845 global_secondary_indexes,
846 local_secondary_indexes,
847 vector_indexes,
848 billing_mode: raw.billing_mode,
849 provisioned_throughput,
850 stream_specification: raw.stream_specification,
851 sse_specification: raw.sse_specification,
852 table_class: raw.table_class,
853 tags: raw.tags,
854 deletion_protection_enabled: raw.deletion_protection_enabled,
855 on_demand_throughput: raw.on_demand_throughput,
856 })
857}
858
859fn collect_pt_errors(pt_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
862 if let Some(v) = pt_val
863 && let Some(obj) = v.as_object()
864 {
865 let wcu = obj.get("WriteCapacityUnits");
866 let rcu = obj.get("ReadCapacityUnits");
867 if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
868 errors.push("Value null at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
869 } else if let Some(w) = wcu.and_then(|v| v.as_i64())
870 && w < 1
871 {
872 errors.push(format!("Value '{}' at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w));
873 }
874 if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
875 errors.push("Value null at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
876 } else if let Some(r) = rcu.and_then(|v| v.as_i64())
877 && r < 1
878 {
879 errors.push(format!("Value '{}' at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r));
880 }
881 }
882}
883
884fn collect_ks_errors(ks_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
885 match ks_val {
886 None => {
887 errors.push(
888 "Value null at 'keySchema' failed to satisfy constraint: Member must not be null"
889 .to_string(),
890 );
891 }
892 Some(v) => {
893 if let Some(arr) = v.as_array() {
894 if arr.is_empty() {
895 errors.push("Value '[]' at 'keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1".to_string());
896 } else if arr.len() > 2 {
897 let dump = render_key_schema_java_toString(arr);
898 errors.push(format!("Value '{}' at 'keySchema' failed to satisfy constraint: Member must have length less than or equal to 2", dump));
899 }
900 for (i, elem) in arr.iter().enumerate().take(10) {
901 collect_ks_elem_errors(elem, i + 1, errors);
902 }
903 }
904 }
905 }
906}
907
908fn collect_ks_elem_errors(elem: &serde_json::Value, idx: usize, errors: &mut Vec<String>) {
909 if let Some(obj) = elem.as_object() {
910 if !obj.contains_key("AttributeName")
911 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
912 {
913 errors.push(format!("Value null at 'keySchema.{}.member.attributeName' failed to satisfy constraint: Member must not be null", idx));
914 }
915 let kt = obj.get("KeyType");
916 if kt.is_none() || kt == Some(&serde_json::Value::Null) {
917 errors.push(format!("Value null at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must not be null", idx));
918 } else if let Some(s) = kt.and_then(|v| v.as_str())
919 && s != "HASH"
920 && s != "RANGE"
921 {
922 errors.push(format!("Value '{}' at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must satisfy enum value set: [HASH, RANGE]", s, idx));
923 }
924 }
925}
926
927#[allow(non_snake_case)]
933fn render_key_schema_java_toString(arr: &[serde_json::Value]) -> String {
934 let parts: Vec<String> = arr
935 .iter()
936 .map(|elem| {
937 let an = elem
938 .get("AttributeName")
939 .and_then(|v| v.as_str())
940 .unwrap_or("");
941 let kt = elem.get("KeyType").and_then(|v| v.as_str()).unwrap_or("");
942 format!("KeySchemaElement(attributeName={an}, keyType={kt})")
943 })
944 .collect();
945 format!("[{}]", parts.join(", "))
946}
947
948fn collect_ad_errors(ad_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
949 match ad_val {
950 None => {
951 errors.push("Value null at 'attributeDefinitions' failed to satisfy constraint: Member must not be null".to_string());
952 }
953 Some(v) => {
954 if let Some(arr) = v.as_array() {
955 for (i, elem) in arr.iter().enumerate() {
956 if let Some(obj) = elem.as_object() {
957 if !obj.contains_key("AttributeName")
958 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
959 {
960 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeName' failed to satisfy constraint: Member must not be null", i + 1));
961 }
962 let at = obj.get("AttributeType");
963 if at.is_none() || at == Some(&serde_json::Value::Null) {
964 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must not be null", i + 1));
965 } else if let Some(s) = at.and_then(|v| v.as_str())
966 && s != "S"
967 && s != "N"
968 && s != "B"
969 {
970 errors.push(format!("Value '{}' at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must satisfy enum value set: [B, N, S]", s, i + 1));
971 }
972 }
973 }
974 }
975 }
976 }
977}
978
979fn collect_lsi_errors(lsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
980 if let Some(v) = lsi_val
981 && let Some(arr) = v.as_array()
982 {
983 for (i, elem) in arr.iter().enumerate().take(10) {
984 if let Some(obj) = elem.as_object() {
985 if !obj.contains_key("IndexName")
987 || obj.get("IndexName") == Some(&serde_json::Value::Null)
988 {
989 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
990 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
991 collect_idx_name_errors(name, "localSecondaryIndexes", i + 1, errors);
992 }
993 if !obj.contains_key("KeySchema")
994 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
995 {
996 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
997 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array())
998 && ks.is_empty()
999 {
1000 errors.push(format!("Value '[]' at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
1001 }
1002 if !obj.contains_key("Projection")
1003 || obj.get("Projection") == Some(&serde_json::Value::Null)
1004 {
1005 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
1006 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
1007 collect_proj_errors(
1008 p,
1009 &format!("localSecondaryIndexes.{}.member", i + 1),
1010 errors,
1011 );
1012 }
1013 }
1014 }
1015 }
1016}
1017
1018fn collect_gsi_errors(gsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
1019 if let Some(v) = gsi_val
1020 && let Some(arr) = v.as_array()
1021 {
1022 for (i, elem) in arr.iter().enumerate().take(10) {
1023 if let Some(obj) = elem.as_object() {
1024 if !obj.contains_key("KeySchema")
1026 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
1027 {
1028 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
1029 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array())
1030 && ks.is_empty()
1031 {
1032 errors.push(format!("Value '[]' at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
1033 }
1034 if !obj.contains_key("Projection")
1035 || obj.get("Projection") == Some(&serde_json::Value::Null)
1036 {
1037 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
1038 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
1039 collect_proj_errors(
1040 p,
1041 &format!("globalSecondaryIndexes.{}.member", i + 1),
1042 errors,
1043 );
1044 }
1045 if !obj.contains_key("IndexName")
1046 || obj.get("IndexName") == Some(&serde_json::Value::Null)
1047 {
1048 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
1049 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
1050 collect_idx_name_errors(name, "globalSecondaryIndexes", i + 1, errors);
1051 }
1052 if let Some(pt) = obj.get("ProvisionedThroughput").and_then(|v| v.as_object()) {
1054 let wcu = pt.get("WriteCapacityUnits");
1055 let rcu = pt.get("ReadCapacityUnits");
1056 if let Some(w) = wcu.and_then(|v| v.as_i64()) {
1057 if w < 1 {
1058 errors.push(format!("Value '{}' at 'globalSecondaryIndexes.{}.member.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w, i + 1));
1059 }
1060 } else if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
1061 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
1062 }
1063 if let Some(r) = rcu.and_then(|v| v.as_i64()) {
1064 if r < 1 {
1065 errors.push(format!("Value '{}' at 'globalSecondaryIndexes.{}.member.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r, i + 1));
1066 }
1067 } else if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
1068 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
1069 }
1070 }
1071 }
1072 }
1073 }
1074}
1075
1076#[cfg(feature = "mcp-server")]
1085pub(crate) fn vector_indexes_request_model_error(
1086 vix_val: &Option<serde_json::Value>,
1087) -> Option<String> {
1088 let mut errors = Vec::new();
1089 collect_vix_errors(vix_val, &mut errors);
1090 errors.truncate(10);
1091 if errors.is_empty() {
1092 return None;
1093 }
1094 Some(format!(
1095 "{} validation error{} detected: {}",
1096 errors.len(),
1097 if errors.len() == 1 { "" } else { "s" },
1098 errors.join("; ")
1099 ))
1100}
1101
1102fn collect_vix_errors(vix_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
1108 let arr = match vix_val.as_ref().and_then(|v| v.as_array()) {
1109 Some(arr) => arr,
1110 None => return,
1111 };
1112 for (i, elem) in arr.iter().enumerate().take(10) {
1113 let obj = match elem.as_object() {
1114 Some(obj) => obj,
1115 None => continue,
1116 };
1117 collect_vix_obj_errors(obj, &format!("vectorIndexes.{}.member", i + 1), errors);
1118 }
1119}
1120
1121pub(crate) fn collect_vix_obj_errors(
1127 obj: &serde_json::Map<String, serde_json::Value>,
1128 path: &str,
1129 errors: &mut Vec<String>,
1130) {
1131 if !obj.contains_key("IndexName") || obj.get("IndexName") == Some(&serde_json::Value::Null) {
1132 errors.push(format!(
1133 "Value null at '{path}.indexName' failed to satisfy constraint: Member must not be null"
1134 ));
1135 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
1136 collect_idx_name_errors_at(name, path, errors);
1137 }
1138 if !obj.contains_key("VectorAttribute")
1139 || obj.get("VectorAttribute") == Some(&serde_json::Value::Null)
1140 {
1141 errors.push(format!(
1142 "Value null at '{path}.vectorAttribute' failed to satisfy constraint: Member must not be null"
1143 ));
1144 } else if let Some(va) = obj.get("VectorAttribute").and_then(|v| v.as_object()) {
1145 if !va.contains_key("AttributeName")
1146 || va.get("AttributeName") == Some(&serde_json::Value::Null)
1147 {
1148 errors.push(format!(
1149 "Value null at '{path}.vectorAttribute.attributeName' failed to satisfy constraint: Member must not be null"
1150 ));
1151 } else if let Some(name) = va.get("AttributeName").and_then(|v| v.as_str()) {
1152 if name.is_empty() {
1157 errors.push(format!(
1158 "Value '' at '{path}.vectorAttribute.attributeName' failed to satisfy constraint: Member must have length greater than or equal to 1"
1159 ));
1160 }
1161 }
1162 }
1163 if let Some(schema) = obj.get("SearchSchema").and_then(|v| v.as_array()) {
1164 for (j, se) in schema.iter().enumerate().take(10) {
1165 let seo = match se.as_object() {
1166 Some(seo) => seo,
1167 None => continue,
1168 };
1169 let sidx = j + 1;
1170 if !seo.contains_key("AttributeName")
1171 || seo.get("AttributeName") == Some(&serde_json::Value::Null)
1172 {
1173 errors.push(format!(
1174 "Value null at '{path}.searchSchema.{sidx}.member.attributeName' failed to satisfy constraint: Member must not be null"
1175 ));
1176 }
1177 let et = seo.get("SearchSchemaElementType");
1178 if et.is_none() || et == Some(&serde_json::Value::Null) {
1179 errors.push(format!(
1180 "Value null at '{path}.searchSchema.{sidx}.member.searchSchemaElementType' failed to satisfy constraint: Member must not be null"
1181 ));
1182 } else if let Some(s) = et.and_then(|v| v.as_str())
1183 && s != "HASH"
1184 && s != "INLINE_FILTER"
1185 {
1186 errors.push(format!(
1187 "Value '{s}' at '{path}.searchSchema.{sidx}.member.searchSchemaElementType' failed to satisfy constraint: Member must satisfy enum value set: [HASH, INLINE_FILTER]"
1188 ));
1189 }
1190 }
1191 }
1192 if !obj.contains_key("Projection") || obj.get("Projection") == Some(&serde_json::Value::Null) {
1193 errors.push(format!(
1194 "Value null at '{path}.projection' failed to satisfy constraint: Member must not be null"
1195 ));
1196 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
1197 collect_proj_errors(p, path, errors);
1198 }
1199 let dims = obj.get("Dimensions");
1200 if dims.is_none() || dims == Some(&serde_json::Value::Null) {
1201 errors.push(format!(
1202 "Value null at '{path}.dimensions' failed to satisfy constraint: Member must not be null"
1203 ));
1204 } else if let Some(d) = dims.and_then(truncated_dimensions) {
1205 if d < 1 {
1211 errors.push(format!(
1212 "Value '{d}' at '{path}.dimensions' failed to satisfy constraint: Member must have value greater than or equal to 1"
1213 ));
1214 }
1215 }
1216 let df = obj.get("DistanceFunction");
1217 if df.is_none() || df == Some(&serde_json::Value::Null) {
1218 errors.push(format!(
1219 "Value null at '{path}.distanceFunction' failed to satisfy constraint: Member must not be null"
1220 ));
1221 } else if let Some(s) = df.and_then(|v| v.as_str())
1222 && s != "COSINE"
1223 && s != "DOT_PRODUCT"
1224 && s != "EUCLIDEAN"
1225 {
1226 errors.push(format!(
1227 "Value '{s}' at '{path}.distanceFunction' failed to satisfy constraint: Member must satisfy enum value set: [COSINE, DOT_PRODUCT, EUCLIDEAN]"
1228 ));
1229 }
1230}
1231
1232fn truncated_dimensions(v: &serde_json::Value) -> Option<i64> {
1238 if let Some(d) = v.as_i64() {
1239 return Some(d);
1240 }
1241 if v.as_u64().is_some() {
1242 return Some(i64::MAX);
1244 }
1245 v.as_f64().map(|f| f.trunc() as i64)
1247}
1248
1249pub(crate) fn normalise_vix_dimensions(vix_val: &serde_json::Value) -> serde_json::Value {
1256 let mut val = vix_val.clone();
1257 if let Some(arr) = val.as_array_mut() {
1258 for elem in arr {
1259 if let Some(obj) = elem.as_object_mut() {
1260 normalise_vix_dimensions_obj(obj);
1261 }
1262 }
1263 }
1264 val
1265}
1266
1267pub(crate) fn normalise_vix_dimensions_obj(obj: &mut serde_json::Map<String, serde_json::Value>) {
1271 let key = if obj.contains_key("Dimensions") {
1275 "Dimensions"
1276 } else {
1277 "dimensions"
1278 };
1279 let Some(dims) = obj.get_mut(key) else {
1280 return;
1281 };
1282 if let Some(d) = truncated_dimensions(dims) {
1283 *dims = serde_json::Value::from(d.clamp(0, i64::from(u32::MAX)) as u32);
1284 }
1285}
1286
1287fn collect_idx_name_errors(name: &str, prefix: &str, idx: usize, errors: &mut Vec<String>) {
1288 collect_idx_name_errors_at(name, &format!("{prefix}.{idx}.member"), errors);
1289}
1290
1291fn collect_idx_name_errors_at(name: &str, path: &str, errors: &mut Vec<String>) {
1294 if !name
1295 .chars()
1296 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
1297 {
1298 errors.push(format!("Value '{}' at '{}.indexName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+", name, path));
1299 }
1300 if name.len() < 3 {
1301 errors.push(format!("Value '{}' at '{}.indexName' failed to satisfy constraint: Member must have length greater than or equal to 3", name, path));
1302 }
1303 if name.len() > 255 {
1304 errors.push(format!("Value '{}' at '{}.indexName' failed to satisfy constraint: Member must have length less than or equal to 255", name, path));
1305 }
1306}
1307
1308fn collect_proj_errors(
1312 proj: &serde_json::Map<String, serde_json::Value>,
1313 prefix: &str,
1314 errors: &mut Vec<String>,
1315) {
1316 if let Some(pt) = proj.get("ProjectionType")
1317 && let Some(s) = pt.as_str()
1318 && s != "ALL"
1319 && s != "KEYS_ONLY"
1320 && s != "INCLUDE"
1321 {
1322 errors.push(format!("Value '{}' at '{}.projection.projectionType' failed to satisfy constraint: Member must satisfy enum value set: [ALL, INCLUDE, KEYS_ONLY]", s, prefix));
1323 }
1324 if let Some(nka) = proj.get("NonKeyAttributes")
1325 && let Some(arr) = nka.as_array()
1326 && arr.is_empty()
1327 {
1328 errors.push(format!("Value '[]' at '{}.projection.nonKeyAttributes' failed to satisfy constraint: Member must have length greater than or equal to 1", prefix));
1329 }
1330}
1331
1332fn validate_key_schema_structure(ks: &[KeySchemaElement]) -> std::result::Result<(), String> {
1335 if ks.is_empty() {
1336 return Err("1 validation error detected: Value null at 'keySchema' failed to satisfy constraint: Member must have length less than or equal to 2".to_string());
1337 }
1338 if ks[0].key_type != KeyType::HASH {
1339 return Err(
1340 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
1341 );
1342 }
1343 if ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name {
1344 return Err(
1345 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
1346 .to_string(),
1347 );
1348 }
1349 if ks.len() == 2 && ks[1].key_type != KeyType::RANGE {
1350 return Err(
1351 "Invalid KeySchema: The second KeySchemaElement is not a RANGE key type".to_string(),
1352 );
1353 }
1354 Ok(())
1355}
1356
1357fn validate_key_attrs_in_defs(
1358 ks: &[KeySchemaElement],
1359 defs: &[AttributeDefinition],
1360) -> std::result::Result<(), String> {
1361 let missing: Vec<&str> = ks
1363 .iter()
1364 .filter(|k| !defs.iter().any(|d| d.attribute_name == k.attribute_name))
1365 .map(|k| k.attribute_name.as_str())
1366 .collect();
1367
1368 if missing.is_empty() {
1369 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
1372 if has_dup_names {
1373 return Err(
1374 "Invalid KeySchema: Some index key attribute have no definition".to_string(),
1375 );
1376 }
1377 return Ok(());
1378 }
1379
1380 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
1386 let has_dup_types = ks.len() == 2 && ks[0].key_type == ks[1].key_type;
1387 let use_generic = defs.is_empty() || ks.len() >= 2 || has_dup_names || has_dup_types;
1388
1389 if use_generic {
1390 return Err("Invalid KeySchema: Some index key attribute have no definition".to_string());
1391 }
1392
1393 let key_names: Vec<&str> = missing.to_vec();
1395 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1396 Err(format!(
1397 "One or more parameter values were invalid: Some index key attributes are not defined in \
1398 AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
1399 key_names.join(", "),
1400 def_names.join(", ")
1401 ))
1402}
1403
1404fn validate_attr_def_count(
1405 ks: &[KeySchemaElement],
1406 defs: &[AttributeDefinition],
1407 lsis: &Option<Vec<LocalSecondaryIndex>>,
1408 gsis: &Option<Vec<GlobalSecondaryIndex>>,
1409 vixs: &Option<Vec<VectorIndex>>,
1410) -> std::result::Result<(), String> {
1411 let mut all_key_attrs = std::collections::HashSet::new();
1412 for k in ks {
1413 all_key_attrs.insert(k.attribute_name.as_str());
1414 }
1415 if let Some(lsis) = lsis {
1416 for lsi in lsis {
1417 for k in &lsi.key_schema {
1418 all_key_attrs.insert(k.attribute_name.as_str());
1419 }
1420 }
1421 }
1422 if let Some(gsis) = gsis {
1423 for gsi in gsis {
1424 for k in &gsi.key_schema {
1425 all_key_attrs.insert(k.attribute_name.as_str());
1426 }
1427 }
1428 }
1429 if let Some(vixs) = vixs {
1433 for vix in vixs {
1434 if let Some(ref schema) = vix.search_schema {
1435 for elem in schema {
1436 all_key_attrs.insert(elem.attribute_name.as_str());
1437 }
1438 }
1439 }
1440 }
1441 if defs.len() != all_key_attrs.len() {
1442 return Err("One or more parameter values were invalid: Number of attributes in KeySchema does not exactly match number of attributes defined in AttributeDefinitions".to_string());
1443 }
1444 Ok(())
1445}
1446
1447fn validate_lsi_list(
1448 lsis: &[LocalSecondaryIndex],
1449 ks: &[KeySchemaElement],
1450 defs: &[AttributeDefinition],
1451) -> std::result::Result<(), String> {
1452 if !ks.iter().any(|k| k.key_type == KeyType::RANGE) {
1455 return Err("One or more parameter values were invalid: Table KeySchema does not have a range key, which is required when specifying a LocalSecondaryIndex".to_string());
1456 }
1457
1458 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1460 let mut missing_keys = Vec::new();
1461 for lsi in lsis {
1462 for k in &lsi.key_schema {
1463 if !def_names.contains(&k.attribute_name.as_str())
1464 && !missing_keys.contains(&k.attribute_name.as_str())
1465 {
1466 missing_keys.push(k.attribute_name.as_str());
1467 }
1468 }
1469 }
1470 if !missing_keys.is_empty() {
1471 let mut all_keys = Vec::new();
1472 for lsi in lsis {
1473 for k in &lsi.key_schema {
1474 if !all_keys.contains(&k.attribute_name.as_str()) {
1475 all_keys.push(k.attribute_name.as_str());
1476 }
1477 }
1478 }
1479 return Err(format!(
1480 "One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
1481 all_keys.join(", "),
1482 def_names.join(", ")
1483 ));
1484 }
1485
1486 for lsi in lsis {
1488 validate_lsi_structure(lsi, ks)?;
1489 }
1490
1491 let mut seen = std::collections::HashSet::new();
1493 for lsi in lsis {
1494 if !seen.insert(&lsi.index_name) {
1495 return Err(format!(
1496 "One or more parameter values were invalid: Duplicate index name: {}",
1497 lsi.index_name
1498 ));
1499 }
1500 }
1501
1502 if lsis.len() > 5 {
1504 return Err("One or more parameter values were invalid: Number of LocalSecondaryIndexes exceeds per-table limit of 5".to_string());
1505 }
1506
1507 Ok(())
1508}
1509
1510fn validate_gsi_list(
1511 gsis: &[GlobalSecondaryIndex],
1512 defs: &[AttributeDefinition],
1513 bm: &str,
1514) -> std::result::Result<(), String> {
1515 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1519 let mut missing_keys = Vec::new();
1520 for gsi in gsis {
1521 for k in &gsi.key_schema {
1522 if !def_names.contains(&k.attribute_name.as_str())
1523 && !missing_keys.contains(&k.attribute_name.as_str())
1524 {
1525 missing_keys.push(k.attribute_name.as_str());
1526 }
1527 }
1528 }
1529 if !missing_keys.is_empty() {
1530 let mut all_keys = Vec::new();
1531 for gsi in gsis {
1532 for k in &gsi.key_schema {
1533 if !all_keys.contains(&k.attribute_name.as_str()) {
1534 all_keys.push(k.attribute_name.as_str());
1535 }
1536 }
1537 }
1538 return Err(format!(
1539 "One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
1540 all_keys.join(", "),
1541 def_names.join(", ")
1542 ));
1543 }
1544
1545 for gsi in gsis {
1547 validate_gsi_structure(gsi)?;
1548 }
1549
1550 let mut seen = std::collections::HashSet::new();
1552 for gsi in gsis {
1553 if !seen.insert(&gsi.index_name) {
1554 return Err(format!(
1555 "One or more parameter values were invalid: Duplicate index name: {}",
1556 gsi.index_name
1557 ));
1558 }
1559 }
1560
1561 if gsis.len() > 20 {
1563 return Err("One or more parameter values were invalid: GlobalSecondaryIndex count exceeds the per-table limit of 20".to_string());
1564 }
1565
1566 if bm == "PAY_PER_REQUEST" {
1568 for gsi in gsis {
1569 if gsi.provisioned_throughput.is_some() {
1570 return Err(format!(
1571 "One or more parameter values were invalid: ProvisionedThroughput should not be specified for index: {} when BillingMode is PAY_PER_REQUEST",
1572 gsi.index_name
1573 ));
1574 }
1575 }
1576 }
1577
1578 Ok(())
1579}
1580
1581fn validate_lsi_structure(
1582 lsi: &LocalSecondaryIndex,
1583 table_ks: &[KeySchemaElement],
1584) -> std::result::Result<(), String> {
1585 validate_key_schema_structure(&lsi.key_schema)?;
1587
1588 let lsi_sk = lsi.key_schema.iter().find(|k| k.key_type == KeyType::RANGE);
1590 if lsi_sk.is_none() {
1591 return Err(format!(
1592 "One or more parameter values were invalid: Index KeySchema does not have a range key for index: {}",
1593 lsi.index_name
1594 ));
1595 }
1596
1597 let table_pk = table_ks
1599 .iter()
1600 .find(|k| k.key_type == KeyType::HASH)
1601 .map(|k| k.attribute_name.as_str());
1602 let lsi_pk = lsi
1603 .key_schema
1604 .iter()
1605 .find(|k| k.key_type == KeyType::HASH)
1606 .map(|k| k.attribute_name.as_str());
1607 if lsi_pk != table_pk {
1608 return Err(format!(
1609 "One or more parameter values were invalid: \
1610 Index KeySchema does not have the same leading hash key as table KeySchema \
1611 for index: {}. index hash key: {}, table hash key: {}",
1612 lsi.index_name,
1613 lsi_pk.unwrap_or("null"),
1614 table_pk.unwrap_or("null")
1615 ));
1616 }
1617
1618 validate_proj_structure(&lsi.projection)?;
1620
1621 Ok(())
1622}
1623
1624fn validate_gsi_structure(gsi: &GlobalSecondaryIndex) -> std::result::Result<(), String> {
1625 validate_key_schema_structure(&gsi.key_schema)?;
1626 validate_proj_structure(&gsi.projection)?;
1627 Ok(())
1628}
1629
1630fn validate_proj_structure(p: &Projection) -> std::result::Result<(), String> {
1631 match &p.projection_type {
1636 None => Err(
1637 "One or more parameter values were invalid: Unknown ProjectionType: null".to_string(),
1638 ),
1639 Some(ProjectionType::ALL) => match &p.non_key_attributes {
1640 Some(_) => Err("One or more parameter values were invalid: ProjectionType is ALL, but NonKeyAttributes is specified".to_string()),
1641 None => Ok(()),
1642 },
1643 Some(ProjectionType::KEYS_ONLY) => match &p.non_key_attributes {
1644 Some(_) => Err("One or more parameter values were invalid: ProjectionType is KEYS_ONLY, but NonKeyAttributes is specified".to_string()),
1645 None => Ok(()),
1646 },
1647 Some(ProjectionType::INCLUDE) => match &p.non_key_attributes {
1648 None => Err("One or more parameter values were invalid: ProjectionType is INCLUDE, but NonKeyAttributes is not specified".to_string()),
1649 Some(nka) if nka.is_empty() => Err("One or more parameter values were invalid: NonKeyAttributes must not be empty".to_string()),
1650 Some(_) => Ok(()),
1651 },
1652 }
1653}