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
4 changes: 2 additions & 2 deletions crates/core/src/expression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ pub use parser::{parse_condition, parse_condition_with_depth_limit};
pub use projection::{apply_projection, parse_projection};
pub use reserved_words::validate_no_reserved_words;
pub use resolver::{
ExpressionMaps, collect_key_condition_refs, resolve_element_name, resolve_name_ref,
resolve_path, validate_begins_with_operands, validate_unused_attributes,
ExpressionMaps, collect_key_condition_refs, collect_value_placeholders, resolve_element_name,
resolve_name_ref, resolve_path, validate_begins_with_operands, validate_unused_attributes,
};
pub use tokenizer::{Token, tokenize, tokenize_for, tokenize_with_limit};
pub use update_evaluator::apply_update;
Expand Down
110 changes: 109 additions & 1 deletion crates/core/src/expression/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,46 @@ fn collect_expr_refs(
}
}

/// Walk an `Expr` tree and append every `Expr::Placeholder(name)` reference to `out`.
///
/// Used by `UpdateItem` depth validation: for each `SET` action's right-hand
/// side, collect the EAV placeholders referenced (directly or via
/// `if_not_exists`, `list_append`, arithmetic, etc.). Resolving those names
/// against the `ExpressionMaps` yields the set of attribute values that will
/// be stored, so their nesting depth must be validated.
pub fn collect_value_placeholders(expr: &super::ast::Expr, out: &mut Vec<String>) {
use super::ast::Expr;
match expr {
Expr::Placeholder(name) => out.push(name.clone()),
Expr::Path(_) => {}
Expr::Compare { left, right, .. } | Expr::Arithmetic { left, right, .. } => {
collect_value_placeholders(left, out);
collect_value_placeholders(right, out);
}
Expr::And(l, r) | Expr::Or(l, r) => {
collect_value_placeholders(l, out);
collect_value_placeholders(r, out);
}
Expr::Not(inner) => collect_value_placeholders(inner, out),
Expr::Function { args, .. } => {
for a in args {
collect_value_placeholders(a, out);
}
}
Expr::Between { operand, low, high } => {
collect_value_placeholders(operand, out);
collect_value_placeholders(low, out);
collect_value_placeholders(high, out);
}
Expr::In { operand, list } => {
collect_value_placeholders(operand, out);
for i in list {
collect_value_placeholders(i, out);
}
}
}
}

fn collect_action_refs(
action: &super::ast::UpdateAction,
names: &mut std::collections::HashSet<String>,
Expand Down Expand Up @@ -411,7 +451,7 @@ pub fn validate_begins_with_operands(
#[cfg(test)]
mod tests {
use super::*;
use crate::expression::ast::Expr;
use crate::expression::ast::{ArithOp, Expr, PathElement};
use crate::expression::parser::parse_condition;
use crate::expression::tokenizer::tokenize;
use std::collections::BTreeSet;
Expand Down Expand Up @@ -534,4 +574,72 @@ mod tests {
let err = validate_begins_with_operands(&expr, &maps).unwrap_err();
assert!(err.to_string().contains("operand type: N"));
}

fn placeholder(s: &str) -> Expr {
Expr::Placeholder(s.to_owned())
}

#[test]
fn collect_value_placeholders_finds_direct_reference() {
let mut out = Vec::new();
collect_value_placeholders(&placeholder(":d"), &mut out);
assert_eq!(out, vec![":d".to_owned()]);
}

#[test]
fn collect_value_placeholders_walks_function_args() {
// SET v = if_not_exists(path, :default)
let expr = Expr::Function {
name: "if_not_exists".to_owned(),
args: vec![
Expr::Path(vec![PathElement::Attribute("path".to_owned())]),
placeholder(":default"),
],
};
let mut out = Vec::new();
collect_value_placeholders(&expr, &mut out);
assert_eq!(out, vec![":default".to_owned()]);
}

#[test]
fn collect_value_placeholders_walks_arithmetic() {
// SET v = :a + :b
let expr = Expr::Arithmetic {
left: Box::new(placeholder(":a")),
op: ArithOp::Add,
right: Box::new(placeholder(":b")),
};
let mut out = Vec::new();
collect_value_placeholders(&expr, &mut out);
assert_eq!(out, vec![":a".to_owned(), ":b".to_owned()]);
}

#[test]
fn collect_value_placeholders_walks_nested_function() {
// SET v = list_append(:base, list_append(:extra, :more))
let expr = Expr::Function {
name: "list_append".to_owned(),
args: vec![
placeholder(":base"),
Expr::Function {
name: "list_append".to_owned(),
args: vec![placeholder(":extra"), placeholder(":more")],
},
],
};
let mut out = Vec::new();
collect_value_placeholders(&expr, &mut out);
assert_eq!(
out,
vec![":base".to_owned(), ":extra".to_owned(), ":more".to_owned()]
);
}

#[test]
fn collect_value_placeholders_path_only_yields_nothing() {
let expr = Expr::Path(vec![PathElement::Attribute("address".to_owned())]);
let mut out = Vec::new();
collect_value_placeholders(&expr, &mut out);
assert!(out.is_empty());
}
}
207 changes: 207 additions & 0 deletions crates/core/src/validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ pub fn validate_put_item(
validate_item_keys(&input.item, key_schema, attr_defs)?;
validate_attribute_name_sizes(&input.item, limits)?;
validate_item_numbers(&input.item)?;
validate_item_nesting_depth(&input.item)?;

let size = item_size_bytes(&input.item);
if size > limits.max_item_size_bytes {
Expand Down Expand Up @@ -535,6 +536,11 @@ pub fn validate_update_item(
) -> Result<(), DynamoDbError> {
validate_table_name(&input.table_name, limits)?;
validate_key_only(&input.key, key_schema, attr_defs)?;

if let Some(updates) = &input.attribute_updates {
validate_attribute_values_nesting_depth(updates.values().filter_map(|u| u.value.as_ref()))?;
}

Ok(())
}

Expand Down Expand Up @@ -837,6 +843,63 @@ fn validate_attribute_number(value: &AttributeValue) -> Result<(), DynamoDbError
Ok(())
}

/// Maximum total nesting levels (M/L wrappers plus the leaf) DynamoDB allows.
pub(crate) const MAX_ITEM_NESTING_DEPTH: usize = 32;

/// Validate that no attribute value in `item` nests beyond `MAX_ITEM_NESTING_DEPTH`.
pub fn validate_item_nesting_depth(item: &Item) -> Result<(), DynamoDbError> {
for value in item.values() {
check_attribute_value_depth(value, 0)?;
}
Ok(())
}

/// Validate nesting depth on attribute values introduced outside of an `Item`
/// (`ExpressionAttributeValues`, `AttributeUpdates`, `Expected`).
pub fn validate_attribute_values_nesting_depth<'a, I>(values: I) -> Result<(), DynamoDbError>
where
I: IntoIterator<Item = &'a AttributeValue>,
{
for v in values {
check_attribute_value_depth(v, 0)?;
}
Ok(())
}

fn check_attribute_value_depth(
value: &AttributeValue,
current_depth: usize,
) -> Result<(), DynamoDbError> {
match value {
AttributeValue::M(map) => {
let next = current_depth + 1;
if next >= MAX_ITEM_NESTING_DEPTH {
return Err(nesting_depth_error());
}
for v in map.values() {
check_attribute_value_depth(v, next)?;
}
}
AttributeValue::L(list) => {
let next = current_depth + 1;
if next >= MAX_ITEM_NESTING_DEPTH {
return Err(nesting_depth_error());
}
for v in list {
check_attribute_value_depth(v, next)?;
}
}
_ => {}
}
Ok(())
}

fn nesting_depth_error() -> DynamoDbError {
DynamoDbError::ValidationException(
"Nesting Levels have exceeded supported limits: Attributes in the item have nested levels beyond supported limit".to_owned(),
)
}

fn validate_unique_index_names(input: &CreateTableInput) -> Result<(), DynamoDbError> {
let mut names = std::collections::HashSet::new();
if let Some(gsis) = &input.global_secondary_indexes {
Expand Down Expand Up @@ -1230,4 +1293,148 @@ mod tests {
input.update_expression = Some(String::new());
assert!(validate_update_item(&input, &limits, &key_schema, &attr_defs).is_ok());
}

fn nested_map(depth: usize) -> AttributeValue {
let mut leaf = AttributeValue::S("leaf".to_owned());
for _ in 0..depth {
let mut m = std::collections::BTreeMap::new();
m.insert("a".to_owned(), leaf);
leaf = AttributeValue::M(m);
}
leaf
}

fn nested_list(depth: usize) -> AttributeValue {
let mut leaf = AttributeValue::S("leaf".to_owned());
for _ in 0..depth {
leaf = AttributeValue::L(vec![leaf]);
}
leaf
}

#[test]
fn nesting_depth_at_limit_accepted() {
// 31 wrappers + leaf = 32 total levels, DynamoDB's hard cap.
let mut item = Item::new();
item.insert("deep".to_owned(), nested_map(MAX_ITEM_NESTING_DEPTH - 1));
validate_item_nesting_depth(&item).expect("32 total levels must be accepted");
}

#[test]
fn nesting_depth_one_over_limit_rejected_for_map() {
let mut item = Item::new();
item.insert("deep".to_owned(), nested_map(MAX_ITEM_NESTING_DEPTH));
let err = validate_item_nesting_depth(&item).unwrap_err();
assert!(
err.to_string()
.contains("Nesting Levels have exceeded supported limits"),
"unexpected error: {err}"
);
}

#[test]
fn nesting_depth_one_over_limit_rejected_for_list() {
let mut item = Item::new();
item.insert("deep".to_owned(), nested_list(MAX_ITEM_NESTING_DEPTH));
let err = validate_item_nesting_depth(&item).unwrap_err();
assert!(
err.to_string()
.contains("Nesting Levels have exceeded supported limits"),
"unexpected error: {err}"
);
}

#[test]
fn nesting_depth_mixed_map_and_list_counted_together() {
let mut leaf = AttributeValue::S("leaf".to_owned());
for i in 0..MAX_ITEM_NESTING_DEPTH {
leaf = if i % 2 == 0 {
AttributeValue::L(vec![leaf])
} else {
let mut m = std::collections::BTreeMap::new();
m.insert("a".to_owned(), leaf);
AttributeValue::M(m)
};
}
let mut item = Item::new();
item.insert("deep".to_owned(), leaf);
let err = validate_item_nesting_depth(&item).unwrap_err();
assert!(
err.to_string()
.contains("Nesting Levels have exceeded supported limits"),
"unexpected error: {err}"
);
}

#[test]
fn nesting_depth_attribute_values_iterator_at_limit_accepted() {
let v = nested_map(MAX_ITEM_NESTING_DEPTH - 1);
validate_attribute_values_nesting_depth(std::iter::once(&v))
.expect("32 total levels via iterator must be accepted");
}

#[test]
fn nesting_depth_attribute_values_iterator_one_over_rejected() {
let v = nested_map(MAX_ITEM_NESTING_DEPTH);
let err = validate_attribute_values_nesting_depth(std::iter::once(&v)).unwrap_err();
assert!(
err.to_string()
.contains("Nesting Levels have exceeded supported limits"),
"unexpected error: {err}"
);
}

#[test]
fn nesting_depth_visits_all_top_level_attributes() {
// Only one of three top-level attributes is over the limit. The
// recursion must inspect every attribute and reject.
let mut item = Item::new();
item.insert("shallow_a".to_owned(), AttributeValue::S("a".to_owned()));
item.insert("deep".to_owned(), nested_map(MAX_ITEM_NESTING_DEPTH));
item.insert("shallow_b".to_owned(), AttributeValue::N("42".to_owned()));
let err = validate_item_nesting_depth(&item).unwrap_err();
assert!(
err.to_string()
.contains("Nesting Levels have exceeded supported limits"),
"unexpected error: {err}"
);
}

#[test]
fn nesting_depth_visits_all_map_children() {
// A wide Map: many children, only one is over the limit.
let mut wide = std::collections::BTreeMap::new();
wide.insert("a".to_owned(), AttributeValue::S("x".to_owned()));
wide.insert("b".to_owned(), nested_map(MAX_ITEM_NESTING_DEPTH - 1));
wide.insert("c".to_owned(), nested_map(MAX_ITEM_NESTING_DEPTH));
wide.insert("d".to_owned(), AttributeValue::N("1".to_owned()));
let mut item = Item::new();
item.insert("wide".to_owned(), AttributeValue::M(wide));
let err = validate_item_nesting_depth(&item).unwrap_err();
assert!(
err.to_string()
.contains("Nesting Levels have exceeded supported limits"),
"unexpected error: {err}"
);
}

#[test]
fn nesting_depth_visits_all_list_elements() {
// A wide List: many elements, only one element is over the limit.
let wide = vec![
AttributeValue::S("x".to_owned()),
nested_map(MAX_ITEM_NESTING_DEPTH - 1),
AttributeValue::Bool(true),
nested_map(MAX_ITEM_NESTING_DEPTH),
AttributeValue::N("3".to_owned()),
];
let mut item = Item::new();
item.insert("wide".to_owned(), AttributeValue::L(wide));
let err = validate_item_nesting_depth(&item).unwrap_err();
assert!(
err.to_string()
.contains("Nesting Levels have exceeded supported limits"),
"unexpected error: {err}"
);
}
}
3 changes: 2 additions & 1 deletion crates/engine/src/batch_write_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use extenddb_core::types::{
};
use extenddb_core::validation::{
validate_attribute_name_sizes, validate_batch_item_keys, validate_batch_key_only,
validate_item_size, validate_key_sizes,
validate_item_nesting_depth, validate_item_size, validate_key_sizes,
};

use crate::OperationContext;
Expand Down Expand Up @@ -123,6 +123,7 @@ pub async fn handle_batch_write_item(
&key_info.key_schema,
&key_info.attribute_definitions,
)?;
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)?;
Expand Down
Loading
Loading