Skip to content
Open
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
169 changes: 169 additions & 0 deletions crates/core/src/validation/key_definitions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright 2026 ExtendDB contributors
// SPDX-License-Identifier: Apache-2.0

//! Checks that a key schema's attributes are defined in `AttributeDefinitions`,
//! shared by `CreateTable` and an `UpdateTable` GSI `Create`.
//!
//! The service's rules, measured on 2026-10-25 and 2026-10-26 (us-east-1):
//!
//! - A name defined twice answers `Attribute Name is duplicated: <name>`,
//! naming the first name whose second definition comes first in the list.
//! The service checks it after the table billing checks, the one-name-twice
//! rule and the length rule below, and before the other key schema checks;
//! ExtendDB checks it after all key schema shape checks.
//! - Each key schema is then checked in turn: the table's, each LSI's, each
//! GSI's. A schema with more keys than there are definitions answers
//! `Invalid KeySchema: Some index key attribute have no definition`. A
//! schema with a key that has no definition answers `Some index key
//! attributes are not defined in AttributeDefinitions`, listing every key of
//! that schema and every definition.
//!
//! The service lists the keys and definitions in an order that follows neither
//! the request nor the alphabet; ExtendDB lists them in request order.

use std::collections::HashSet;

use crate::error::DynamoDbError;
use crate::types::{AttributeDefinition, KeySchemaElement};

/// Refuse a name defined twice in `defs`.
///
/// # Errors
///
/// `ValidationException` `Attribute Name is duplicated: <name>`.
pub fn validate_unique_attribute_definitions(
defs: &[AttributeDefinition],
) -> Result<(), DynamoDbError> {
let mut seen = HashSet::with_capacity(defs.len());
for def in defs {
if !seen.insert(def.attribute_name.as_str()) {
return Err(DynamoDbError::ValidationException(format!(
"Attribute Name is duplicated: {}",
def.attribute_name
)));
}
}
Ok(())
}

/// Refuse a key schema whose keys are not all defined in `defs`.
///
/// # Errors
///
/// `ValidationException` with the `Invalid KeySchema` sentence when the schema
/// has more keys than `defs` has entries, otherwise with the `Some index key
/// attributes are not defined` sentence when any key has no definition.
pub fn validate_key_schema_defined(
key_schema: &[KeySchemaElement],
defs: &[AttributeDefinition],
) -> Result<(), DynamoDbError> {
if defs.len() < key_schema.len() {
return Err(DynamoDbError::ValidationException(
"Invalid KeySchema: Some index key attribute have no definition".to_owned(),
));
}
if key_schema
.iter()
.all(|k| defs.iter().any(|d| d.attribute_name == k.attribute_name))
{
return Ok(());
}
let keys: Vec<&str> = key_schema
.iter()
.map(|k| k.attribute_name.as_str())
.collect();
let names: Vec<&str> = defs.iter().map(|d| d.attribute_name.as_str()).collect();
Err(DynamoDbError::ValidationException(format!(
"One or more parameter values were invalid: Some index key attributes are not defined in \
AttributeDefinitions. Keys: [{}], AttributeDefinitions: [{}]",
keys.join(", "),
names.join(", ")
)))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::types::{KeyType, ScalarAttributeType};

fn defs(names: &[&str]) -> Vec<AttributeDefinition> {
names
.iter()
.map(|n| AttributeDefinition {
attribute_name: (*n).to_owned(),
attribute_type: ScalarAttributeType::S,
})
.collect()
}

fn ks(hash: &str, range: Option<&str>) -> Vec<KeySchemaElement> {
let mut k = vec![KeySchemaElement {
attribute_name: hash.to_owned(),
key_type: KeyType::Hash,
}];
if let Some(r) = range {
k.push(KeySchemaElement {
attribute_name: r.to_owned(),
key_type: KeyType::Range,
});
}
k
}

fn message(r: Result<(), DynamoDbError>) -> String {
match r {
Err(DynamoDbError::ValidationException(m)) => m,
other => panic!("expected a ValidationException, got {other:?}"),
}
}

const INVALID: &str = "Invalid KeySchema: Some index key attribute have no definition";

#[test]
fn the_first_repeated_name_is_named() {
assert_eq!(
message(validate_unique_attribute_definitions(&defs(&[
"b", "a", "b", "a"
]))),
"Attribute Name is duplicated: b"
);
assert!(validate_unique_attribute_definitions(&defs(&["a", "b"])).is_ok());
}

#[test]
fn fewer_definitions_than_keys_is_the_key_schema_sentence() {
assert_eq!(
message(validate_key_schema_defined(
&ks("g", Some("r")),
&defs(&["pk"])
)),
INVALID
);
assert_eq!(
message(validate_key_schema_defined(&ks("g", None), &[])),
INVALID
);
}

#[test]
fn a_missing_key_lists_every_key_and_every_definition() {
assert_eq!(
message(validate_key_schema_defined(
&ks("g", Some("r")),
&defs(&["pk", "g"])
)),
"One or more parameter values were invalid: Some index key attributes are not defined \
in AttributeDefinitions. Keys: [g, r], AttributeDefinitions: [pk, g]"
);
// As many definitions as keys, none of them a key.
assert_eq!(
message(validate_key_schema_defined(
&ks("g", Some("r")),
&defs(&["pk", "z"])
)),
"One or more parameter values were invalid: Some index key attributes are not defined \
in AttributeDefinitions. Keys: [g, r], AttributeDefinitions: [pk, z]"
);
assert!(validate_key_schema_defined(&ks("g", Some("r")), &defs(&["r", "g"])).is_ok());
}
}
31 changes: 15 additions & 16 deletions crates/core/src/validation/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright 2026 ExtendDB contributors
// SPDX-License-Identifier: Apache-2.0
mod key_definitions;
pub mod number;
pub mod vector_item;

pub use key_definitions::{validate_key_schema_defined, validate_unique_attribute_definitions};
pub use vector_item::{
MAX_HASH_KEY_SIZE, MAX_INLINE_FILTER_SIZE, validate_vector_write,
validate_vector_write_changed, vector_components, vector_norm,
Expand Down Expand Up @@ -101,8 +103,10 @@ pub fn validate_create_table(
validate_key_schema(input, limits.allow_multipart_table_keys)?;
validate_gsi_key_schemas(input)?;
validate_lsi_key_schemas(input)?;
validate_attribute_definitions(input)?;
// The service reports a table billing fault before any definition fault
// (measured 2026-10-26).
validate_provisioned_throughput(input)?;
validate_attribute_definitions(input)?;
validate_gsi_provisioned_throughput(input)?;
validate_gsi_throughput_minimum(input)?;
validate_gsi_count(input, limits)?;
Expand Down Expand Up @@ -943,14 +947,16 @@ fn validate_attribute_definitions(input: &CreateTableInput) -> Result<(), Dynamo
}
}

// Every key attribute must have a definition
for attr in &key_attrs {
if !def_names.contains(attr) {
return Err(DynamoDbError::ValidationException(format!(
"One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [{attr}], AttributeDefinitions: [{}]",
format_attr_defs(&input.attribute_definitions)
)));
}
// Every key attribute must have a definition: the table's key schema, then
// each LSI's, then each GSI's (measured order, see key_definitions.rs).
validate_unique_attribute_definitions(&input.attribute_definitions)?;
let defs = &input.attribute_definitions;
validate_key_schema_defined(&input.key_schema, defs)?;
for lsi in input.local_secondary_indexes.iter().flatten() {
validate_key_schema_defined(&lsi.key_schema, defs)?;
}
for gsi in input.global_secondary_indexes.iter().flatten() {
validate_key_schema_defined(&gsi.key_schema, defs)?;
}

// Every definition must be used by a key
Expand All @@ -966,13 +972,6 @@ fn validate_attribute_definitions(input: &CreateTableInput) -> Result<(), Dynamo
Ok(())
}

fn format_attr_defs(defs: &[AttributeDefinition]) -> String {
defs.iter()
.map(|d| d.attribute_name.clone())
.collect::<Vec<_>>()
.join(", ")
}

/// Validate table-level throughput against the effective billing mode.
///
/// # Errors
Expand Down
47 changes: 9 additions & 38 deletions crates/engine/src/update_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,46 +251,17 @@ fn validate_gsi_updates(input: &UpdateTableInput) -> Result<(), DynamoDbError> {
}
// Every key attribute must be in AttributeDefinitions. The
// service's sentence depends on the member's shape (measured
// 2026-10-25): absent names the index; present but lacking
// the key lists the keys and the definitions (the CreateTable
// sentence, whose CreateTable copy lives in core's validation
// and is not shared with this path); an empty list has its
// own sentence.
let missing: Vec<&str> = match &input.attribute_definitions {
None => {
return Err(DynamoDbError::ValidationException(format!(
"One or more parameter values were invalid: AttributeDefinitions is not specified for index: {}",
create.index_name
)));
}
Some(defs) if defs.is_empty() => {
return Err(DynamoDbError::ValidationException(
"Invalid KeySchema: Some index key attribute have no definition"
.to_owned(),
));
}
Some(defs) => create
.key_schema
.iter()
.filter(|ks| !defs.iter().any(|ad| ad.attribute_name == ks.attribute_name))
.map(|ks| ks.attribute_name.as_str())
.collect(),
};
if let Some(first_missing) = missing.first() {
let defined: Vec<&str> = input
.attribute_definitions
.as_deref()
.unwrap_or(&[])
.iter()
.map(|ad| ad.attribute_name.as_str())
.collect();
// 2026-10-25 and 2026-10-26): absent names the index; a name
// defined twice is refused first; then the CreateTable rules
// for the index's key schema (core's key_definitions.rs).
let Some(defs) = &input.attribute_definitions else {
return Err(DynamoDbError::ValidationException(format!(
"One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. \
Keys: [{}], AttributeDefinitions: [{}]",
first_missing,
defined.join(", ")
"One or more parameter values were invalid: AttributeDefinitions is not specified for index: {}",
create.index_name
)));
}
};
extenddb_core::validation::validate_unique_attribute_definitions(defs)?;
extenddb_core::validation::validate_key_schema_defined(&create.key_schema, defs)?;
}
if let Some(delete) = &update.delete {
extenddb_core::validation::validate_index_name(&delete.index_name)?;
Expand Down
23 changes: 16 additions & 7 deletions crates/engine/src/update_table_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ fn enabling_a_stream_without_a_view_type_is_refused() {

/// A created index whose key attribute is not in AttributeDefinitions is
/// refused with the sentence the service picks by the member's shape
/// (measured 2026-10-25): absent names the index, present but lacking the
/// key lists the key and the definitions, empty has its own sentence.
/// (measured 2026-10-25 and 2026-10-26): absent names the index; fewer
/// definitions than keys, including none, has the key schema sentence;
/// otherwise the sentence lists every key and every definition.
#[test]
fn a_created_gsi_on_an_undefined_key_attribute_is_refused_by_member_shape() {
let input = |defs: Option<Value>| -> UpdateTableInput {
Expand Down Expand Up @@ -72,13 +73,21 @@ fn a_created_gsi_on_an_undefined_key_attribute_is_refused_by_member_shape() {
message(Some(
serde_json::json!([{ "AttributeName": "g", "AttributeType": "S" }])
)),
"One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [r], AttributeDefinitions: [g]"
"Invalid KeySchema: Some index key attribute have no definition"
);
assert_eq!(
message(Some(
serde_json::json!([{ "AttributeName": "other", "AttributeType": "S" }])
)),
"One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [g], AttributeDefinitions: [other]"
message(Some(serde_json::json!([
{ "AttributeName": "pk", "AttributeType": "S" },
{ "AttributeName": "g", "AttributeType": "S" },
]))),
"One or more parameter values were invalid: Some index key attributes are not defined in AttributeDefinitions. Keys: [g, r], AttributeDefinitions: [pk, g]"
);
assert_eq!(
message(Some(serde_json::json!([
{ "AttributeName": "g", "AttributeType": "S" },
{ "AttributeName": "g", "AttributeType": "S" },
]))),
"Attribute Name is duplicated: g"
);
validate_gsi_updates(&input(Some(serde_json::json!([
{ "AttributeName": "g", "AttributeType": "S" },
Expand Down
1 change: 1 addition & 0 deletions docs/differences-from-dynamodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ adaptation when switching between ExtendDB and the real service.
|------|----------|------|
| Table creation delay | Returns `CREATING` immediately; transitions to `ACTIVE` typically within seconds. Same behavior for on-demand and provisioned | Configurable via `control_plane_delay_seconds` runtime setting (default: 5s) |
| DeletionProtectionEnabled | Enforced | Enforced (accepted and stored, DeleteTable rejects when enabled) |
| Order of the lists in `Some index key attributes are not defined in AttributeDefinitions. Keys: [...], AttributeDefinitions: [...]` (CreateTable, UpdateTable GSI Create) | Every key of the schema and every definition, in an order that follows neither the request nor the alphabet and does not depend on the attribute types (measured 2026-10-26: definitions `[pk, r]` render `[r, pk]`, keys `hk, rk` render `[rk, hk]`) | The same keys and definitions, the keys in key schema order and the definitions in request order. A client that reads the first element of `Keys` can see a different name |

## Time to Live (TTL)

Expand Down
Loading
Loading