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_gsi_errors(&raw.global_secondary_indexes, &mut errors);
743 collect_lsi_errors(&raw.local_secondary_indexes, &mut errors);
744 collect_vix_errors(&raw.vector_indexes, &mut errors);
745
746 errors.truncate(10);
748
749 if !errors.is_empty() {
750 let prefix = format!(
751 "{} validation error{} detected: ",
752 errors.len(),
753 if errors.len() == 1 { "" } else { "s" }
754 );
755 return Err(format!("{}{}", prefix, errors.join("; ")));
756 }
757
758 let billing_mode_str = raw.billing_mode.as_deref().unwrap_or("PROVISIONED");
760 if billing_mode_str == "PAY_PER_REQUEST" && raw.provisioned_throughput.is_some() {
761 return Err(
762 "One or more parameter values were invalid: Neither ReadCapacityUnits nor \
763 WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"
764 .to_string(),
765 );
766 }
767
768 if let Some(ref pt) = raw.provisioned_throughput
770 && let Some(obj) = pt.as_object()
771 {
772 let rcu = obj
773 .get("ReadCapacityUnits")
774 .and_then(|v| v.as_i64())
775 .unwrap_or(0);
776 let wcu = obj
777 .get("WriteCapacityUnits")
778 .and_then(|v| v.as_i64())
779 .unwrap_or(0);
780 const MAX_THROUGHPUT: i64 = 1_000_000_000_000;
781 if rcu > MAX_THROUGHPUT {
782 return Err(format!(
783 "Given value {} for ReadCapacityUnits is out of bounds",
784 rcu
785 ));
786 }
787 if wcu > MAX_THROUGHPUT {
788 return Err(format!(
789 "Given value {} for WriteCapacityUnits is out of bounds",
790 wcu
791 ));
792 }
793 }
794
795 if raw.billing_mode.as_deref() == Some("PROVISIONED") && raw.provisioned_throughput.is_none() {
797 return Err(
798 "One or more parameter values were invalid: ReadCapacityUnits and \
799 WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
800 .to_string(),
801 );
802 }
803
804 let key_schema: Vec<KeySchemaElement> = raw
806 .key_schema
807 .as_ref()
808 .map(|v| serde_json::from_value(v.clone()))
809 .transpose()
810 .map_err(|e| e.to_string())?
811 .unwrap_or_default();
812 let attribute_definitions: Vec<AttributeDefinition> = raw
813 .attribute_definitions
814 .as_ref()
815 .map(|v| serde_json::from_value(v.clone()))
816 .transpose()
817 .map_err(|e| e.to_string())?
818 .unwrap_or_default();
819 let provisioned_throughput: Option<ProvisionedThroughput> = raw
820 .provisioned_throughput
821 .as_ref()
822 .map(|v| serde_json::from_value(v.clone()))
823 .transpose()
824 .map_err(|e| e.to_string())?;
825 let global_secondary_indexes: Option<Vec<GlobalSecondaryIndex>> = raw
826 .global_secondary_indexes
827 .as_ref()
828 .map(|v| serde_json::from_value(v.clone()))
829 .transpose()
830 .map_err(|e| e.to_string())?;
831 let local_secondary_indexes: Option<Vec<LocalSecondaryIndex>> = raw
832 .local_secondary_indexes
833 .as_ref()
834 .map(|v| serde_json::from_value(v.clone()))
835 .transpose()
836 .map_err(|e| e.to_string())?;
837 let vector_indexes: Option<Vec<VectorIndex>> = raw
838 .vector_indexes
839 .as_ref()
840 .map(|v| serde_json::from_value(normalise_vix_dimensions(v)))
841 .transpose()
842 .map_err(|e| e.to_string())?;
843
844 Ok(CreateTableRequest {
845 table_name,
846 key_schema,
847 attribute_definitions,
848 global_secondary_indexes,
849 local_secondary_indexes,
850 vector_indexes,
851 billing_mode: raw.billing_mode,
852 provisioned_throughput,
853 stream_specification: raw.stream_specification,
854 sse_specification: raw.sse_specification,
855 table_class: raw.table_class,
856 tags: raw.tags,
857 deletion_protection_enabled: raw.deletion_protection_enabled,
858 on_demand_throughput: raw.on_demand_throughput,
859 })
860}
861
862fn collect_pt_errors(pt_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
865 if let Some(v) = pt_val
866 && let Some(obj) = v.as_object()
867 {
868 let wcu = obj.get("WriteCapacityUnits");
869 let rcu = obj.get("ReadCapacityUnits");
870 if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
871 errors.push("Value null at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
872 } else if let Some(w) = wcu.and_then(|v| v.as_i64())
873 && w < 1
874 {
875 errors.push(format!("Value '{}' at 'provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", w));
876 }
877 if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
878 errors.push("Value null at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null".to_string());
879 } else if let Some(r) = rcu.and_then(|v| v.as_i64())
880 && r < 1
881 {
882 errors.push(format!("Value '{}' at 'provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must have value greater than or equal to 1", r));
883 }
884 }
885}
886
887fn collect_ks_errors(ks_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
888 match ks_val {
889 None => {
890 errors.push(
891 "Value null at 'keySchema' failed to satisfy constraint: Member must not be null"
892 .to_string(),
893 );
894 }
895 Some(v) => {
896 if let Some(arr) = v.as_array() {
897 if arr.is_empty() {
898 errors.push("Value '[]' at 'keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1".to_string());
899 } else if arr.len() > 2 {
900 let dump = render_key_schema_java_toString(arr);
901 errors.push(format!("Value '{}' at 'keySchema' failed to satisfy constraint: Member must have length less than or equal to 2", dump));
902 }
903 for (i, elem) in arr.iter().enumerate().take(10) {
904 collect_ks_elem_errors(elem, i + 1, errors);
905 }
906 }
907 }
908 }
909}
910
911fn collect_ks_elem_errors(elem: &serde_json::Value, idx: usize, errors: &mut Vec<String>) {
912 if let Some(obj) = elem.as_object() {
913 if !obj.contains_key("AttributeName")
914 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
915 {
916 errors.push(format!("Value null at 'keySchema.{}.member.attributeName' failed to satisfy constraint: Member must not be null", idx));
917 }
918 let kt = obj.get("KeyType");
919 if kt.is_none() || kt == Some(&serde_json::Value::Null) {
920 errors.push(format!("Value null at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must not be null", idx));
921 } else if let Some(s) = kt.and_then(|v| v.as_str())
922 && s != "HASH"
923 && s != "RANGE"
924 {
925 errors.push(format!("Value '{}' at 'keySchema.{}.member.keyType' failed to satisfy constraint: Member must satisfy enum value set: [HASH, RANGE]", s, idx));
926 }
927 }
928}
929
930#[allow(non_snake_case)]
936fn render_key_schema_java_toString(arr: &[serde_json::Value]) -> String {
937 let parts: Vec<String> = arr
938 .iter()
939 .map(|elem| {
940 let an = elem
941 .get("AttributeName")
942 .and_then(|v| v.as_str())
943 .unwrap_or("");
944 let kt = elem.get("KeyType").and_then(|v| v.as_str()).unwrap_or("");
945 format!("KeySchemaElement(attributeName={an}, keyType={kt})")
946 })
947 .collect();
948 format!("[{}]", parts.join(", "))
949}
950
951fn collect_ad_errors(ad_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
952 match ad_val {
953 None => {
954 errors.push("Value null at 'attributeDefinitions' failed to satisfy constraint: Member must not be null".to_string());
955 }
956 Some(v) => {
957 if let Some(arr) = v.as_array() {
958 for (i, elem) in arr.iter().enumerate() {
959 if let Some(obj) = elem.as_object() {
960 if !obj.contains_key("AttributeName")
961 || obj.get("AttributeName") == Some(&serde_json::Value::Null)
962 {
963 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeName' failed to satisfy constraint: Member must not be null", i + 1));
964 }
965 let at = obj.get("AttributeType");
966 if at.is_none() || at == Some(&serde_json::Value::Null) {
967 errors.push(format!("Value null at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must not be null", i + 1));
968 } else if let Some(s) = at.and_then(|v| v.as_str())
969 && s != "S"
970 && s != "N"
971 && s != "B"
972 {
973 errors.push(format!("Value '{}' at 'attributeDefinitions.{}.member.attributeType' failed to satisfy constraint: Member must satisfy enum value set: [B, N, S]", s, i + 1));
974 }
975 }
976 }
977 }
978 }
979 }
980}
981
982fn collect_lsi_errors(lsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
983 if let Some(v) = lsi_val
984 && let Some(arr) = v.as_array()
985 {
986 for (i, elem) in arr.iter().enumerate().take(10) {
987 if let Some(obj) = elem.as_object() {
988 if !obj.contains_key("IndexName")
990 || obj.get("IndexName") == Some(&serde_json::Value::Null)
991 {
992 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
993 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
994 collect_idx_name_errors(name, "localSecondaryIndexes", i + 1, errors);
995 }
996 if !obj.contains_key("KeySchema")
997 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
998 {
999 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
1000 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array())
1001 && ks.is_empty()
1002 {
1003 errors.push(format!("Value '[]' at 'localSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
1004 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array())
1005 && ks.len() > 2
1006 {
1007 errors.push(format!("Value '{}' at 'keySchema' failed to satisfy constraint: Member must have length less than or equal to 2", render_key_schema_java_toString(ks)));
1011 }
1012 if !obj.contains_key("Projection")
1013 || obj.get("Projection") == Some(&serde_json::Value::Null)
1014 {
1015 errors.push(format!("Value null at 'localSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
1016 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
1017 collect_proj_errors(
1018 p,
1019 &format!("localSecondaryIndexes.{}.member", i + 1),
1020 errors,
1021 );
1022 }
1023 }
1024 }
1025 }
1026}
1027
1028fn collect_gsi_errors(gsi_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
1029 if let Some(v) = gsi_val
1030 && let Some(arr) = v.as_array()
1031 {
1032 for (i, elem) in arr.iter().enumerate().take(10) {
1033 if let Some(obj) = elem.as_object() {
1034 if !obj.contains_key("KeySchema")
1036 || obj.get("KeySchema") == Some(&serde_json::Value::Null)
1037 {
1038 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must not be null", i + 1));
1039 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array())
1040 && ks.is_empty()
1041 {
1042 errors.push(format!("Value '[]' at 'globalSecondaryIndexes.{}.member.keySchema' failed to satisfy constraint: Member must have length greater than or equal to 1", i + 1));
1043 } else if let Some(ks) = obj.get("KeySchema").and_then(|v| v.as_array())
1044 && ks.len() > 8
1045 {
1046 errors.push(format!("Value '{}' at 'keySchema' failed to satisfy constraint: Member must have length less than or equal to 8", render_key_schema_java_toString(ks)));
1050 }
1051 if !obj.contains_key("Projection")
1052 || obj.get("Projection") == Some(&serde_json::Value::Null)
1053 {
1054 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.projection' failed to satisfy constraint: Member must not be null", i + 1));
1055 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
1056 collect_proj_errors(
1057 p,
1058 &format!("globalSecondaryIndexes.{}.member", i + 1),
1059 errors,
1060 );
1061 }
1062 if !obj.contains_key("IndexName")
1063 || obj.get("IndexName") == Some(&serde_json::Value::Null)
1064 {
1065 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.indexName' failed to satisfy constraint: Member must not be null", i + 1));
1066 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
1067 collect_idx_name_errors(name, "globalSecondaryIndexes", i + 1, errors);
1068 }
1069 if let Some(pt) = obj.get("ProvisionedThroughput").and_then(|v| v.as_object()) {
1071 let wcu = pt.get("WriteCapacityUnits");
1072 let rcu = pt.get("ReadCapacityUnits");
1073 if let Some(w) = wcu.and_then(|v| v.as_i64()) {
1074 if w < 1 {
1075 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));
1076 }
1077 } else if wcu.is_none() || wcu == Some(&serde_json::Value::Null) {
1078 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.writeCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
1079 }
1080 if let Some(r) = rcu.and_then(|v| v.as_i64()) {
1081 if r < 1 {
1082 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));
1083 }
1084 } else if rcu.is_none() || rcu == Some(&serde_json::Value::Null) {
1085 errors.push(format!("Value null at 'globalSecondaryIndexes.{}.member.provisionedThroughput.readCapacityUnits' failed to satisfy constraint: Member must not be null", i + 1));
1086 }
1087 }
1088 }
1089 }
1090 }
1091}
1092
1093#[cfg(feature = "mcp-server")]
1102pub(crate) fn vector_indexes_request_model_error(
1103 vix_val: &Option<serde_json::Value>,
1104) -> Option<String> {
1105 let mut errors = Vec::new();
1106 collect_vix_errors(vix_val, &mut errors);
1107 errors.truncate(10);
1108 if errors.is_empty() {
1109 return None;
1110 }
1111 Some(format!(
1112 "{} validation error{} detected: {}",
1113 errors.len(),
1114 if errors.len() == 1 { "" } else { "s" },
1115 errors.join("; ")
1116 ))
1117}
1118
1119fn collect_vix_errors(vix_val: &Option<serde_json::Value>, errors: &mut Vec<String>) {
1125 let arr = match vix_val.as_ref().and_then(|v| v.as_array()) {
1126 Some(arr) => arr,
1127 None => return,
1128 };
1129 for (i, elem) in arr.iter().enumerate().take(10) {
1130 let obj = match elem.as_object() {
1131 Some(obj) => obj,
1132 None => continue,
1133 };
1134 collect_vix_obj_errors(obj, &format!("vectorIndexes.{}.member", i + 1), errors);
1135 }
1136}
1137
1138pub(crate) fn collect_vix_obj_errors(
1144 obj: &serde_json::Map<String, serde_json::Value>,
1145 path: &str,
1146 errors: &mut Vec<String>,
1147) {
1148 if !obj.contains_key("IndexName") || obj.get("IndexName") == Some(&serde_json::Value::Null) {
1149 errors.push(format!(
1150 "Value null at '{path}.indexName' failed to satisfy constraint: Member must not be null"
1151 ));
1152 } else if let Some(name) = obj.get("IndexName").and_then(|v| v.as_str()) {
1153 collect_idx_name_errors_at(name, path, errors);
1154 }
1155 if !obj.contains_key("VectorAttribute")
1156 || obj.get("VectorAttribute") == Some(&serde_json::Value::Null)
1157 {
1158 errors.push(format!(
1159 "Value null at '{path}.vectorAttribute' failed to satisfy constraint: Member must not be null"
1160 ));
1161 } else if let Some(va) = obj.get("VectorAttribute").and_then(|v| v.as_object()) {
1162 if !va.contains_key("AttributeName")
1163 || va.get("AttributeName") == Some(&serde_json::Value::Null)
1164 {
1165 errors.push(format!(
1166 "Value null at '{path}.vectorAttribute.attributeName' failed to satisfy constraint: Member must not be null"
1167 ));
1168 } else if let Some(name) = va.get("AttributeName").and_then(|v| v.as_str()) {
1169 if name.is_empty() {
1174 errors.push(format!(
1175 "Value '' at '{path}.vectorAttribute.attributeName' failed to satisfy constraint: Member must have length greater than or equal to 1"
1176 ));
1177 }
1178 }
1179 }
1180 if let Some(schema) = obj.get("SearchSchema").and_then(|v| v.as_array()) {
1181 for (j, se) in schema.iter().enumerate().take(10) {
1182 let seo = match se.as_object() {
1183 Some(seo) => seo,
1184 None => continue,
1185 };
1186 let sidx = j + 1;
1187 if !seo.contains_key("AttributeName")
1188 || seo.get("AttributeName") == Some(&serde_json::Value::Null)
1189 {
1190 errors.push(format!(
1191 "Value null at '{path}.searchSchema.{sidx}.member.attributeName' failed to satisfy constraint: Member must not be null"
1192 ));
1193 }
1194 let et = seo.get("SearchSchemaElementType");
1195 if et.is_none() || et == Some(&serde_json::Value::Null) {
1196 errors.push(format!(
1197 "Value null at '{path}.searchSchema.{sidx}.member.searchSchemaElementType' failed to satisfy constraint: Member must not be null"
1198 ));
1199 } else if let Some(s) = et.and_then(|v| v.as_str())
1200 && s != "HASH"
1201 && s != "INLINE_FILTER"
1202 {
1203 errors.push(format!(
1204 "Value '{s}' at '{path}.searchSchema.{sidx}.member.searchSchemaElementType' failed to satisfy constraint: Member must satisfy enum value set: [HASH, INLINE_FILTER]"
1205 ));
1206 }
1207 }
1208 }
1209 if !obj.contains_key("Projection") || obj.get("Projection") == Some(&serde_json::Value::Null) {
1210 errors.push(format!(
1211 "Value null at '{path}.projection' failed to satisfy constraint: Member must not be null"
1212 ));
1213 } else if let Some(p) = obj.get("Projection").and_then(|v| v.as_object()) {
1214 collect_proj_errors(p, path, errors);
1215 }
1216 let dims = obj.get("Dimensions");
1217 if dims.is_none() || dims == Some(&serde_json::Value::Null) {
1218 errors.push(format!(
1219 "Value null at '{path}.dimensions' failed to satisfy constraint: Member must not be null"
1220 ));
1221 } else if let Some(d) = dims.and_then(truncated_dimensions) {
1222 if d < 1 {
1228 errors.push(format!(
1229 "Value '{d}' at '{path}.dimensions' failed to satisfy constraint: Member must have value greater than or equal to 1"
1230 ));
1231 }
1232 }
1233 let df = obj.get("DistanceFunction");
1234 if df.is_none() || df == Some(&serde_json::Value::Null) {
1235 errors.push(format!(
1236 "Value null at '{path}.distanceFunction' failed to satisfy constraint: Member must not be null"
1237 ));
1238 } else if let Some(s) = df.and_then(|v| v.as_str())
1239 && s != "COSINE"
1240 && s != "DOT_PRODUCT"
1241 && s != "EUCLIDEAN"
1242 {
1243 errors.push(format!(
1244 "Value '{s}' at '{path}.distanceFunction' failed to satisfy constraint: Member must satisfy enum value set: [COSINE, DOT_PRODUCT, EUCLIDEAN]"
1245 ));
1246 }
1247}
1248
1249fn truncated_dimensions(v: &serde_json::Value) -> Option<i64> {
1255 if let Some(d) = v.as_i64() {
1256 return Some(d);
1257 }
1258 if v.as_u64().is_some() {
1259 return Some(i64::MAX);
1261 }
1262 v.as_f64().map(|f| f.trunc() as i64)
1264}
1265
1266pub(crate) fn normalise_vix_dimensions(vix_val: &serde_json::Value) -> serde_json::Value {
1273 let mut val = vix_val.clone();
1274 if let Some(arr) = val.as_array_mut() {
1275 for elem in arr {
1276 if let Some(obj) = elem.as_object_mut() {
1277 normalise_vix_dimensions_obj(obj);
1278 }
1279 }
1280 }
1281 val
1282}
1283
1284pub(crate) fn normalise_vix_dimensions_obj(obj: &mut serde_json::Map<String, serde_json::Value>) {
1288 let key = if obj.contains_key("Dimensions") {
1292 "Dimensions"
1293 } else {
1294 "dimensions"
1295 };
1296 let Some(dims) = obj.get_mut(key) else {
1297 return;
1298 };
1299 if let Some(d) = truncated_dimensions(dims) {
1300 *dims = serde_json::Value::from(d.clamp(0, i64::from(u32::MAX)) as u32);
1301 }
1302}
1303
1304fn collect_idx_name_errors(name: &str, prefix: &str, idx: usize, errors: &mut Vec<String>) {
1305 collect_idx_name_errors_at(name, &format!("{prefix}.{idx}.member"), errors);
1306}
1307
1308fn collect_idx_name_errors_at(name: &str, path: &str, errors: &mut Vec<String>) {
1311 if !name
1312 .chars()
1313 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
1314 {
1315 errors.push(format!("Value '{}' at '{}.indexName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+", name, path));
1316 }
1317 if name.len() < 3 {
1318 errors.push(format!("Value '{}' at '{}.indexName' failed to satisfy constraint: Member must have length greater than or equal to 3", name, path));
1319 }
1320 if name.len() > 255 {
1321 errors.push(format!("Value '{}' at '{}.indexName' failed to satisfy constraint: Member must have length less than or equal to 255", name, path));
1322 }
1323}
1324
1325fn collect_proj_errors(
1329 proj: &serde_json::Map<String, serde_json::Value>,
1330 prefix: &str,
1331 errors: &mut Vec<String>,
1332) {
1333 if let Some(pt) = proj.get("ProjectionType")
1334 && let Some(s) = pt.as_str()
1335 && s != "ALL"
1336 && s != "KEYS_ONLY"
1337 && s != "INCLUDE"
1338 {
1339 errors.push(format!("Value '{}' at '{}.projection.projectionType' failed to satisfy constraint: Member must satisfy enum value set: [ALL, INCLUDE, KEYS_ONLY]", s, prefix));
1340 }
1341 if let Some(nka) = proj.get("NonKeyAttributes")
1342 && let Some(arr) = nka.as_array()
1343 && arr.is_empty()
1344 {
1345 errors.push(format!("Value '[]' at '{}.projection.nonKeyAttributes' failed to satisfy constraint: Member must have length greater than or equal to 1", prefix));
1346 }
1347}
1348
1349fn validate_key_schema_structure(ks: &[KeySchemaElement]) -> std::result::Result<(), String> {
1352 if ks.is_empty() {
1353 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());
1354 }
1355 if ks[0].key_type != KeyType::HASH {
1356 return Err(
1357 "Invalid KeySchema: The first KeySchemaElement is not a HASH key type".to_string(),
1358 );
1359 }
1360 if ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name {
1361 return Err(
1362 "Both the Hash Key and the Range Key element in the KeySchema have the same name"
1363 .to_string(),
1364 );
1365 }
1366 if ks.len() == 2 && ks[1].key_type != KeyType::RANGE {
1367 return Err(
1368 "Invalid KeySchema: The second KeySchemaElement is not a RANGE key type".to_string(),
1369 );
1370 }
1371 Ok(())
1372}
1373
1374fn validate_key_attrs_in_defs(
1375 ks: &[KeySchemaElement],
1376 defs: &[AttributeDefinition],
1377) -> std::result::Result<(), String> {
1378 let missing: Vec<&str> = ks
1380 .iter()
1381 .filter(|k| !defs.iter().any(|d| d.attribute_name == k.attribute_name))
1382 .map(|k| k.attribute_name.as_str())
1383 .collect();
1384
1385 if missing.is_empty() {
1386 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
1389 if has_dup_names {
1390 return Err(
1391 "Invalid KeySchema: Some index key attribute have no definition".to_string(),
1392 );
1393 }
1394 return Ok(());
1395 }
1396
1397 let has_dup_names = ks.len() == 2 && ks[0].attribute_name == ks[1].attribute_name;
1403 let has_dup_types = ks.len() == 2 && ks[0].key_type == ks[1].key_type;
1404 let use_generic = defs.is_empty() || ks.len() >= 2 || has_dup_names || has_dup_types;
1405
1406 if use_generic {
1407 return Err("Invalid KeySchema: Some index key attribute have no definition".to_string());
1408 }
1409
1410 let key_names: Vec<&str> = missing.to_vec();
1412 let def_names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1413 Err(format!(
1414 "One or more parameter values were invalid: Some index key attributes are not defined in \
1415 AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
1416 key_names.join(", "),
1417 def_names.join(", ")
1418 ))
1419}
1420
1421fn validate_attr_def_count(
1422 ks: &[KeySchemaElement],
1423 defs: &[AttributeDefinition],
1424 lsis: &Option<Vec<LocalSecondaryIndex>>,
1425 gsis: &Option<Vec<GlobalSecondaryIndex>>,
1426 vixs: &Option<Vec<VectorIndex>>,
1427) -> std::result::Result<(), String> {
1428 let mut all_key_attrs = std::collections::HashSet::new();
1429 for k in ks {
1430 all_key_attrs.insert(k.attribute_name.as_str());
1431 }
1432 if let Some(lsis) = lsis {
1433 for lsi in lsis {
1434 for k in &lsi.key_schema {
1435 all_key_attrs.insert(k.attribute_name.as_str());
1436 }
1437 }
1438 }
1439 if let Some(gsis) = gsis {
1440 for gsi in gsis {
1441 for k in &gsi.key_schema {
1442 all_key_attrs.insert(k.attribute_name.as_str());
1443 }
1444 }
1445 }
1446 if let Some(vixs) = vixs {
1450 for vix in vixs {
1451 if let Some(ref schema) = vix.search_schema {
1452 for elem in schema {
1453 all_key_attrs.insert(elem.attribute_name.as_str());
1454 }
1455 }
1456 }
1457 }
1458 if defs.len() != all_key_attrs.len() {
1459 let has_index = lsis.as_ref().is_some_and(|l| !l.is_empty())
1460 || gsis.as_ref().is_some_and(|g| !g.is_empty());
1461 if !has_index {
1462 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());
1463 }
1464 let defined: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
1467 let mut used: Vec<&str> = ks.iter().map(|k| k.attribute_name.as_str()).collect();
1468 for k in lsis.iter().flatten().flat_map(|l| &l.key_schema) {
1469 used.push(k.attribute_name.as_str());
1470 }
1471 for k in gsis.iter().flatten().flat_map(|g| &g.key_schema) {
1472 used.push(k.attribute_name.as_str());
1473 }
1474 for elem in vixs
1475 .iter()
1476 .flatten()
1477 .filter_map(|v| v.search_schema.as_ref())
1478 .flatten()
1479 {
1480 used.push(elem.attribute_name.as_str());
1481 }
1482 return Err(format!(
1483 "One or more parameter values were invalid: Some AttributeDefinitions are not used. \
1484 AttributeDefinitions: [{}], keys used: [{}]",
1485 crate::validation::java_hash_order(&defined, true).join(", "),
1486 crate::validation::java_hash_order(&used, false).join(", ")
1487 ));
1488 }
1489 Ok(())
1490}
1491
1492fn validate_lsi_list(
1493 lsis: &[LocalSecondaryIndex],
1494 ks: &[KeySchemaElement],
1495 defs: &[AttributeDefinition],
1496) -> std::result::Result<(), String> {
1497 if !ks.iter().any(|k| k.key_type == KeyType::RANGE) {
1500 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());
1501 }
1502
1503 for lsi in lsis {
1505 if let Some(message) = crate::validation::undefined_index_key_error(&lsi.key_schema, defs) {
1506 return Err(message);
1507 }
1508 }
1509
1510 for lsi in lsis {
1512 validate_lsi_structure(lsi, ks)?;
1513 }
1514
1515 let mut seen = std::collections::HashSet::new();
1517 for lsi in lsis {
1518 if !seen.insert(&lsi.index_name) {
1519 return Err(format!(
1520 "One or more parameter values were invalid: Duplicate index name: {}",
1521 lsi.index_name
1522 ));
1523 }
1524 }
1525
1526 if lsis.len() > 5 {
1528 return Err("One or more parameter values were invalid: Number of LocalSecondaryIndexes exceeds per-table limit of 5".to_string());
1529 }
1530
1531 Ok(())
1532}
1533
1534fn validate_gsi_list(
1535 gsis: &[GlobalSecondaryIndex],
1536 defs: &[AttributeDefinition],
1537 bm: &str,
1538) -> std::result::Result<(), String> {
1539 for gsi in gsis {
1543 if let Some(message) = crate::validation::undefined_index_key_error(&gsi.key_schema, defs) {
1544 return Err(message);
1545 }
1546 }
1547
1548 for gsi in gsis {
1550 validate_gsi_structure(gsi)?;
1551 }
1552
1553 let mut seen = std::collections::HashSet::new();
1555 for gsi in gsis {
1556 if !seen.insert(&gsi.index_name) {
1557 return Err(format!(
1558 "One or more parameter values were invalid: Duplicate index name: {}",
1559 gsi.index_name
1560 ));
1561 }
1562 }
1563
1564 if gsis.len() > 20 {
1566 return Err("One or more parameter values were invalid: GlobalSecondaryIndex count exceeds the per-table limit of 20".to_string());
1567 }
1568
1569 if bm == "PAY_PER_REQUEST" {
1571 for gsi in gsis {
1572 if gsi.provisioned_throughput.is_some() {
1573 return Err(format!(
1574 "One or more parameter values were invalid: ProvisionedThroughput should not be specified for index: {} when BillingMode is PAY_PER_REQUEST",
1575 gsi.index_name
1576 ));
1577 }
1578 }
1579 }
1580
1581 Ok(())
1582}
1583
1584fn validate_lsi_structure(
1585 lsi: &LocalSecondaryIndex,
1586 table_ks: &[KeySchemaElement],
1587) -> std::result::Result<(), String> {
1588 validate_key_schema_structure(&lsi.key_schema)?;
1590
1591 let lsi_sk = lsi.key_schema.iter().find(|k| k.key_type == KeyType::RANGE);
1593 if lsi_sk.is_none() {
1594 return Err(format!(
1595 "One or more parameter values were invalid: Index KeySchema does not have a range key for index: {}",
1596 lsi.index_name
1597 ));
1598 }
1599
1600 let table_pk = table_ks
1602 .iter()
1603 .find(|k| k.key_type == KeyType::HASH)
1604 .map(|k| k.attribute_name.as_str());
1605 let lsi_pk = lsi
1606 .key_schema
1607 .iter()
1608 .find(|k| k.key_type == KeyType::HASH)
1609 .map(|k| k.attribute_name.as_str());
1610 if lsi_pk != table_pk {
1611 return Err(format!(
1612 "One or more parameter values were invalid: \
1613 Index KeySchema does not have the same leading hash key as table KeySchema \
1614 for index: {}. index hash key: {}, table hash key: {}",
1615 lsi.index_name,
1616 lsi_pk.unwrap_or("null"),
1617 table_pk.unwrap_or("null")
1618 ));
1619 }
1620
1621 validate_proj_structure(&lsi.projection)?;
1623
1624 Ok(())
1625}
1626
1627fn validate_gsi_structure(gsi: &GlobalSecondaryIndex) -> std::result::Result<(), String> {
1628 crate::validation::check_gsi_key_schema(&gsi.key_schema)?;
1629 validate_proj_structure(&gsi.projection)?;
1630 Ok(())
1631}
1632
1633fn validate_proj_structure(p: &Projection) -> std::result::Result<(), String> {
1634 match &p.projection_type {
1639 None => Err(
1640 "One or more parameter values were invalid: Unknown ProjectionType: null".to_string(),
1641 ),
1642 Some(ProjectionType::ALL) => match &p.non_key_attributes {
1643 Some(_) => Err("One or more parameter values were invalid: ProjectionType is ALL, but NonKeyAttributes is specified".to_string()),
1644 None => Ok(()),
1645 },
1646 Some(ProjectionType::KEYS_ONLY) => match &p.non_key_attributes {
1647 Some(_) => Err("One or more parameter values were invalid: ProjectionType is KEYS_ONLY, but NonKeyAttributes is specified".to_string()),
1648 None => Ok(()),
1649 },
1650 Some(ProjectionType::INCLUDE) => match &p.non_key_attributes {
1651 None => Err("One or more parameter values were invalid: ProjectionType is INCLUDE, but NonKeyAttributes is not specified".to_string()),
1652 Some(nka) if nka.is_empty() => Err("One or more parameter values were invalid: NonKeyAttributes must not be empty".to_string()),
1653 Some(_) => Ok(()),
1654 },
1655 }
1656}