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
589 changes: 542 additions & 47 deletions crates/core/src/validation/mod.rs

Large diffs are not rendered by default.

19 changes: 16 additions & 3 deletions crates/engine/src/batch_write_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use extenddb_core::types::{
WriteRequest, extract_key, item_size_bytes,
};
use extenddb_core::validation::{
validate_attribute_name_sizes, validate_batch_item_keys, validate_batch_key_only,
validate_item_nesting_depth, validate_item_size, validate_key_sizes,
AttributeNameRequestForm, validate_attribute_name_sizes, validate_batch_item_keys,
validate_batch_key_only, validate_item_nesting_depth, validate_item_size, validate_key_sizes,
};

use crate::OperationContext;
Expand Down Expand Up @@ -66,6 +66,20 @@ pub async fn handle_batch_write_item(
}
}

// Attribute names over the size limit are request-shape validation: every
// PutRequest item is checked before any table lookup and before the key
// checks, so a request naming a missing table, or an item missing its key,
// answers with the name refusal.
for reqs in input.request_items.values() {
for put in reqs.iter().filter_map(|wr| wr.put_request.as_ref()) {
validate_attribute_name_sizes(
&put.item,
&ctx.limits,
AttributeNameRequestForm::BatchRequest,
)?;
}
}

let empty_maps = ExpressionMaps::default();
let mut all_icm: HashMap<String, Vec<extenddb_core::types::ItemCollectionMetrics>> =
HashMap::new();
Expand Down Expand Up @@ -99,7 +113,6 @@ pub async fn handle_batch_write_item(
)?;
validate_item_nesting_depth(&put.item)?;
validate_item_size(&put.item, ctx.limits.max_item_size_bytes)?;
validate_attribute_name_sizes(&put.item, &ctx.limits)?;
validate_key_sizes(&put.item, &key_info.key_schema, &ctx.limits)?;
extenddb_core::validation::validate_vector_write(
&put.item,
Expand Down
10 changes: 10 additions & 0 deletions crates/engine/src/delete_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ pub async fn handle_delete_item(

extenddb_core::validation::validate_table_name(&input.table_name, &ctx.limits)?;

// An attribute name over the size limit that arrives through the
// condition's ExpressionAttributeNames is input validation, reported
// ahead of the table lookup and the condition.
extenddb_core::validation::validate_expression_attribute_name_sizes(
input.expression_attribute_names.as_ref(),
input.expression_attribute_values.as_ref(),
&ctx.limits,
extenddb_core::validation::AttributeNameRequestForm::SingleItem,
)?;

// Validate expressions/`Expected` before the existence check,
// so a missing table returns ValidationException, not ResourceNotFoundException.
// Key validation stays after existence (it needs the table's key schema).
Expand Down
6 changes: 5 additions & 1 deletion crates/engine/src/import_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,11 @@ pub async fn handle_import_table(
error_count += 1;
continue;
}
if let Err(e) = validate_attribute_name_sizes(&item, &ctx.limits) {
if let Err(e) = validate_attribute_name_sizes(
&item,
&ctx.limits,
extenddb_core::validation::AttributeNameRequestForm::SingleItem,
) {
tracing::warn!(error = %e, "import: skipping item with oversized attribute name");
error_count += 1;
continue;
Expand Down
16 changes: 16 additions & 0 deletions crates/engine/src/put_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ pub async fn handle_put_item(
}
})?;

// An attribute name over the size limit is input validation: the service
// reports it ahead of the table lookup, so a request naming a missing
// table answers with the name refusal. A name that arrives only through
// the condition's ExpressionAttributeNames is refused the same way.
extenddb_core::validation::validate_attribute_name_sizes(
&input.item,
&ctx.limits,
extenddb_core::validation::AttributeNameRequestForm::SingleItem,
)?;
extenddb_core::validation::validate_expression_attribute_name_sizes(
input.expression_attribute_names.as_ref(),
input.expression_attribute_values.as_ref(),
&ctx.limits,
extenddb_core::validation::AttributeNameRequestForm::SingleItem,
)?;

// Reject mixing legacy and expression parameters, then EAN/EAV supplied
// without a referencing expression.
let has_expression = input
Expand Down
59 changes: 57 additions & 2 deletions crates/engine/src/transact_write_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ use crate::transact_write_helpers::{
use crate::{DispatchMetrics, DispatchResult};
use extenddb_core::error::DynamoDbError;
use extenddb_core::expression::{Expr, ExpressionMaps, PathElement, UpdateAction};
use extenddb_core::limits::LimitsConfig;
use extenddb_core::types::{
CancellationReason, Item, TransactWriteItem, TransactWriteItemsInput, TransactWriteItemsOutput,
};
use extenddb_core::validation::{
validate_attribute_name_sizes, validate_attribute_values_nesting_depth,
AttributeNameRequestForm, validate_attribute_name_sizes,
validate_attribute_values_nesting_depth, validate_expression_attribute_name_sizes,
validate_item_nesting_depth, validate_item_size, validate_key_not_empty,
};

Expand Down Expand Up @@ -75,6 +77,14 @@ pub async fn handle_transact_write_items(
}
}

// Attribute names over the size limit are request-shape validation of
// each member, run for every member in request order before any member's
// table lookup: a request whose first member names a missing table
// answers with a later member's name refusal.
for twi in &input.transact_items {
validate_member_attribute_name_sizes(twi, &ctx.limits)?;
}

// Resolve table key info, validate inputs, parse expressions, and check
// for duplicate targets — all in a single pass to avoid redundant
// `table_key_info` lookups.
Expand Down Expand Up @@ -341,6 +351,52 @@ fn assigned_vector_attributes(actions: &[UpdateAction], maps: &ExpressionMaps) -
assigned
}

/// Check the attribute names one `TransactWriteItems` member carries: the
/// member's expression maps (any member type), then a `Put` member's item
/// names and map keys. An over-limit name fails the whole request with a
/// top-level ValidationException and no cancellation reasons.
///
/// Order, measured on the service (2026-10-21) with over-limit names in two
/// places at once: a `Put` with a long `#n` and a long key in `:c` reports
/// `:c` (values before names, as the `Update` member does), and a `Put` with a
/// long item name and a long `#n` reports `#n` (expression maps before the
/// item). A standalone `PutItem` with a long item name and a long `#n`
/// reports the item, so that handler keeps the opposite order.
fn validate_member_attribute_name_sizes(
twi: &TransactWriteItem,
limits: &LimitsConfig,
) -> Result<(), DynamoDbError> {
let form = AttributeNameRequestForm::TransactionMember;
let (names, values) = if let Some(put) = &twi.put {
(
put.expression_attribute_names.as_ref(),
put.expression_attribute_values.as_ref(),
)
} else if let Some(del) = &twi.delete {
(
del.expression_attribute_names.as_ref(),
del.expression_attribute_values.as_ref(),
)
} else if let Some(upd) = &twi.update {
(
upd.expression_attribute_names.as_ref(),
upd.expression_attribute_values.as_ref(),
)
} else if let Some(cc) = &twi.condition_check {
(
cc.expression_attribute_names.as_ref(),
cc.expression_attribute_values.as_ref(),
)
} else {
return Ok(());
};
validate_expression_attribute_name_sizes(names, values, limits, form)?;
if let Some(put) = &twi.put {
validate_attribute_name_sizes(&put.item, limits, form)?;
}
Ok(())
}

/// Parse and validate a single `TransactWriteItem`, returning a `PreparedOp`.
async fn prepare_write_op(
twi: &TransactWriteItem,
Expand All @@ -353,7 +409,6 @@ async fn prepare_write_op(
.map_err(storage_err_to_dynamo)?;
validate_item_nesting_depth(&put.item)?;
validate_item_size(&put.item, ctx.limits.max_item_size_bytes)?;
validate_attribute_name_sizes(&put.item, &ctx.limits)?;
validate_key_not_empty(&put.item, &key_info.key_schema)?;
let maps = build_expression_maps(
put.expression_attribute_names.as_ref(),
Expand Down
13 changes: 13 additions & 0 deletions crates/engine/src/update_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ pub async fn handle_update_item(

extenddb_core::validation::validate_table_name(&input.table_name, &ctx.limits)?;

// An attribute name over the size limit is input validation: it is
// reported ahead of the table lookup, the expressions, the condition and
// the index-key checks, whichever map the name arrives through.
extenddb_core::validation::validate_expression_attribute_name_sizes(
input.expression_attribute_names.as_ref(),
input.expression_attribute_values.as_ref(),
&ctx.limits,
extenddb_core::validation::AttributeNameRequestForm::SingleItem,
)?;
if let Some(updates) = &input.attribute_updates {
extenddb_core::validation::validate_attribute_updates_name_sizes(updates, &ctx.limits)?;
}

// Validate Update/Condition expressions (and AttributeUpdates
// desugaring) before the existence check; key, item-content nesting, and
// the no-key-update checks stay after (post-existence on Amazon DynamoDB).
Expand Down
6 changes: 3 additions & 3 deletions docs/dynamodb-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Source: [AWS DynamoDB Service Quotas](https://docs.aws.amazon.com/amazondynamodb
| Maximum item size | 400 KB (409,600 bytes) | Enforced | `LimitsConfig::max_item_size_bytes`, validated on PutItem and post-UpdateItem |
| Partition key size | 1–2,048 bytes | Enforced | `validate_key_sizes`, `LimitsConfig::max_partition_key_size_bytes` |
| Sort key size | 1–1,024 bytes | Enforced | `validate_key_sizes`, `LimitsConfig::max_sort_key_size_bytes` |
| Attribute name size | 1–64 KB (65,535 bytes) | Partial | `validate_attribute_name_sizes`, `LimitsConfig::max_attribute_name_bytes`; top-level names and nested map keys at every depth, on PutItem, BatchWriteItem.PutRequest, TransactWriteItems.Put, and ImportTable. Not enforced on UpdateItem or TransactWriteItems.Update at any level. |
| Attribute name size | 1 to 65,535 bytes (64 KB) | Enforced | `validate_attribute_name_sizes`, `validate_expression_attribute_name_sizes`, `validate_attribute_updates_name_sizes`, `LimitsConfig::max_attribute_name_bytes`; UTF-8 bytes, top-level names and map keys at every depth. PutItem, BatchWriteItem.PutRequest, TransactWriteItems.Put and ImportTable check the item. PutItem, UpdateItem, DeleteItem and every TransactWriteItems member (Put, Update, Delete, ConditionCheck) check every `ExpressionAttributeNames` value and every map key inside every `ExpressionAttributeValues` value; UpdateItem also checks the names and values of `AttributeUpdates`. PutItem and UpdateItem report the name ahead of the table lookup; UpdateItem also reports it ahead of the condition and the index-key checks. TransactWriteItems checks every member, and BatchWriteItem every PutRequest, before any table lookup, so neither a missing table (in any member) nor a PutRequest item missing its key is reported first; a transaction fails as a whole with a top-level ValidationException. With an over-limit name in both expression maps, UpdateItem reports the `ExpressionAttributeNames` placeholder and a transacted Update or Put reports the `ExpressionAttributeValues` placeholder; a transacted Put with a long item name and a long `ExpressionAttributeNames` value reports the placeholder, a standalone PutItem the item. The message depends on the request form (`AttributeNameRequestForm`): PutItem, UpdateItem and DeleteItem answer `1 validation error detected: Attribute name is too large, must be less than 65536 bytes`, followed by ` for key #n` when the name came through `ExpressionAttributeNames`; a TransactWriteItems member answers `ExpressionAttributeNames contains invalid value: Attribute name is too large, must be less than 65536 bytes for key #n`, or `ExpressionAttributeValues contains invalid value: ... for key :v` for a map key inside a value; TransactWriteItems.Put and BatchWriteItem.PutRequest answer `Attribute name is too large, must be less than 65536 bytes` for a name in the item. |
| Attribute nesting depth | 32 levels | Enforced | `validate_item_nesting_depth`, applied on PutItem, UpdateItem, BatchWriteItem.PutRequest, TransactWriteItems.Put, ImportTable |
| Number of attributes per item | No practical limit | Enforced | ExtendDB has no per-item attribute count limit |

Expand Down Expand Up @@ -157,7 +157,7 @@ Source: [AWS DynamoDB Service Quotas](https://docs.aws.amazon.com/amazondynamodb
|----------|----------|---------|--------------|-----|
| Throughput | 5 | 0 | 1 | 2 |
| Tables | 4 | 0 | 0 | 0 |
| Items | 5 | 1 | 0 | 0 |
| Items | 6 | 0 | 0 | 0 |
| Secondary Indexes | 5 | 0 | 1 | 0 |
| Query/Scan | 2 | 0 | 5 | 0 |
| Batch Operations | 3 | 0 | 2 | 0 |
Expand All @@ -167,7 +167,7 @@ Source: [AWS DynamoDB Service Quotas](https://docs.aws.amazon.com/amazondynamodb
| Import/Export/Backup | 2 | 2 | 0 | 7 |
| Global Tables | 0 | 0 | 0 | 2 |
| Contributor Insights | 0 | 0 | 0 | 1 |
| **Total** | **31** | **3** | **16** | **13** |
| **Total** | **32** | **2** | **16** | **13** |

### Unenforced Limits Requiring Tracking

Expand Down
Loading
Loading