Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/core/src/arn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ impl ArnFields<'_> {
}
}

/// True when `account` is an account id: exactly 12 ASCII digits.
#[must_use]
pub fn is_account_id(account: &str) -> bool {
account.len() == 12 && account.bytes().all(|b| b.is_ascii_digit())
}

/// Split a string into the five ARN fields after the `arn:` prefix.
///
/// The resource field keeps every `:` and `/` it contains, so a stream ARN's
Expand Down Expand Up @@ -160,6 +166,22 @@ mod tests {
assert_eq!(f.resource, "table/T");
}

#[test]
fn an_account_id_is_twelve_ascii_digits() {
assert!(is_account_id("123456789012"));
for account in [
"",
"12345678901",
"1234567890123",
"12345678901x",
"1234\u{0}",
"12345678901\u{0}",
"\u{661}1234567890",
] {
assert!(!is_account_id(account), "{account:?}");
Comment thread
LeeroyHannigan marked this conversation as resolved.
Dismissed
}
}

#[test]
fn in_deployment_compares_partition_service_and_region_exactly() {
let arn = "arn:aws:dynamodb:us-east-1:1:table/T/stream/2026-10-20T16:17:40.188";
Expand Down
66 changes: 63 additions & 3 deletions crates/engine/src/gsi_updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,31 @@ pub(crate) fn pay_per_request_after(
}
}

/// `validate_index_name` with the pattern clause located at the update's
/// member path, as the service reports it (measured 2026-10-24). The length
/// clauses keep the bare `indexName` location, which is not measured for
/// UpdateTable.
fn validate_update_index_name(
name: &str,
position: usize,
action: &str,
) -> Result<(), DynamoDbError> {
match extenddb_core::validation::validate_index_name(name) {
Err(DynamoDbError::ValidationException(m))
if m.contains("' at 'indexName' ") && m.contains("regular expression pattern") =>
{
Err(DynamoDbError::ValidationException(m.replace(
"' at 'indexName' ",
&format!(
"' at 'globalSecondaryIndexUpdates.{}.member.{action}.indexName' ",
position + 1
),
)))
}
other => other,
}
}

/// Apply the element rules to `input`.
///
/// # Errors
Expand Down Expand Up @@ -171,7 +196,7 @@ pub(crate) fn validate_gsi_updates(input: &UpdateTableInput) -> Result<(), Dynam
seen.push(name);
}
}
for update in updates {
for (position, update) in updates.iter().enumerate() {
let actions = usize::from(update.create.is_some())
+ usize::from(update.update.is_some())
+ usize::from(update.delete.is_some());
Expand Down Expand Up @@ -200,8 +225,12 @@ pub(crate) fn validate_gsi_updates(input: &UpdateTableInput) -> Result<(), Dynam
));
}
}
// Create and Delete names are validated the way CreateTable does,
// except that the pattern clause carries the member's path (measured
// 2026-10-24: `globalSecondaryIndexUpdates.<n>.member.create.indexName`,
// 1-based). The path of an Update's name is not measured.
if let Some(create) = &update.create {
extenddb_core::validation::validate_index_name(&create.index_name)?;
validate_update_index_name(&create.index_name, position, "create")?;
extenddb_core::validation::validate_index_throughput_minimum(
&create.index_name,
create.provisioned_throughput.as_ref(),
Expand Down Expand Up @@ -234,7 +263,7 @@ pub(crate) fn validate_gsi_updates(input: &UpdateTableInput) -> Result<(), Dynam
}
}
if let Some(delete) = &update.delete {
extenddb_core::validation::validate_index_name(&delete.index_name)?;
validate_update_index_name(&delete.index_name, position, "delete")?;
}
}
Ok(())
Expand Down Expand Up @@ -506,4 +535,35 @@ mod tests {
.contains("Some index key attributes are not defined")
);
}

#[test]
fn an_index_name_outside_the_pattern_is_reported_at_its_member_path() {
let cases = [
(
json!([create("g\u{0}2")]),
"globalSecondaryIndexUpdates.1.member.create.indexName",
),
(
json!([
{ "Delete": { "IndexName": "gix" } },
{ "Delete": { "IndexName": "g\u{0}2" } },
]),
"globalSecondaryIndexUpdates.2.member.delete.indexName",
),
];
for (updates, path) in cases {
assert_eq!(
message(validate_gsi_updates(&input(updates))),
format!(
"1 validation error detected: Value 'g\u{0}2' at '{path}' failed to \
satisfy constraint: Member must satisfy regular expression pattern: \
[a-zA-Z0-9_.-]+"
),
"{path}"
);
}
// A length fault keeps the bare location, which is not measured here.
let m = message(validate_gsi_updates(&input(json!([create("ab")]))));
assert!(m.contains("' at 'indexName' "), "{m}");
}
}
Loading
Loading