From 37ebfee3ed9a5307c1950c3a1af06fae5eb172a9 Mon Sep 17 00:00:00 2001 From: Lee Hannigan Date: Sun, 25 Oct 2026 20:53:54 +0000 Subject: [PATCH 1/3] refactor(mongodb): move the condition compiler tests into their own file condition.rs was 759 lines. The test module moves unchanged to condition_tests.rs, loaded with #[path] as backup_engine.rs loads its test files, so the next change can add tests without the file passing 600 lines. --- crates/storage-mongodb/src/condition.rs | 266 +----------------- crates/storage-mongodb/src/condition_tests.rs | 265 +++++++++++++++++ 2 files changed, 267 insertions(+), 264 deletions(-) create mode 100644 crates/storage-mongodb/src/condition_tests.rs diff --git a/crates/storage-mongodb/src/condition.rs b/crates/storage-mongodb/src/condition.rs index c2301b941..3a54744e4 100644 --- a/crates/storage-mongodb/src/condition.rs +++ b/crates/storage-mongodb/src/condition.rs @@ -493,267 +493,5 @@ fn regex_escape(s: &str) -> String { // ============================================================================ #[cfg(test)] -mod tests { - use super::*; - use extenddb_core::expression::ExpressionMaps; - use extenddb_core::types::AttributeValue; - use std::collections::HashMap; - - fn make_maps(names: Vec<(&str, &str)>, values: Vec<(&str, AttributeValue)>) -> ExpressionMaps { - let names_map: HashMap = names - .into_iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - let values_map: HashMap = values - .into_iter() - .map(|(k, v)| (k.to_string(), v)) - .collect(); - ExpressionMaps::new(names_map, values_map) - } - - #[test] - fn test_attribute_exists() { - let maps = make_maps(vec![], vec![]); - let expr = Expr::Function { - name: "attribute_exists".to_string(), - args: vec![Expr::Path(vec![PathElement::Attribute("foo".to_string())])], - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!(filter, doc! { "item_data.foo": { "$exists": true } }); - } - - #[test] - fn test_attribute_not_exists() { - let maps = make_maps(vec![], vec![]); - let expr = Expr::Function { - name: "attribute_not_exists".to_string(), - args: vec![Expr::Path(vec![PathElement::Attribute("bar".to_string())])], - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!(filter, doc! { "item_data.bar": { "$exists": false } }); - } - - #[test] - fn test_equality_comparison_string() { - let maps = make_maps( - vec![], - vec![(":val", AttributeValue::S("hello".to_string()))], - ); - let expr = Expr::Compare { - left: Box::new(Expr::Path(vec![PathElement::Attribute("name".to_string())])), - op: CompareOp::Eq, - right: Box::new(Expr::Placeholder(":val".to_string())), - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!(filter, doc! { "item_data.name.S": "hello" }); - } - - #[test] - fn test_less_than_comparison_number() { - let maps = make_maps(vec![], vec![(":min", AttributeValue::N("100".to_string()))]); - let expr = Expr::Compare { - left: Box::new(Expr::Path(vec![PathElement::Attribute( - "price".to_string(), - )])), - op: CompareOp::Lt, - right: Box::new(Expr::Placeholder(":min".to_string())), - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!(filter, doc! { "item_data.price.N": { "$lt": "100" } }); - } - - #[test] - fn test_and_condition() { - let maps = make_maps( - vec![], - vec![ - (":v1", AttributeValue::S("active".to_string())), - (":v2", AttributeValue::N("5".to_string())), - ], - ); - let expr = Expr::And( - Box::new(Expr::Compare { - left: Box::new(Expr::Path(vec![PathElement::Attribute( - "status".to_string(), - )])), - op: CompareOp::Eq, - right: Box::new(Expr::Placeholder(":v1".to_string())), - }), - Box::new(Expr::Compare { - left: Box::new(Expr::Path(vec![PathElement::Attribute( - "count".to_string(), - )])), - op: CompareOp::Gt, - right: Box::new(Expr::Placeholder(":v2".to_string())), - }), - ); - let filter = condition_to_filter(&expr, &maps).unwrap(); - let expected = doc! { - "$and": [ - { "item_data.status.S": "active" }, - { "item_data.count.N": { "$gt": "5" } } - ] - }; - assert_eq!(filter, expected); - } - - #[test] - fn test_or_condition() { - let maps = make_maps( - vec![], - vec![ - (":a", AttributeValue::S("x".to_string())), - (":b", AttributeValue::S("y".to_string())), - ], - ); - let expr = Expr::Or( - Box::new(Expr::Compare { - left: Box::new(Expr::Path(vec![PathElement::Attribute("f".to_string())])), - op: CompareOp::Eq, - right: Box::new(Expr::Placeholder(":a".to_string())), - }), - Box::new(Expr::Compare { - left: Box::new(Expr::Path(vec![PathElement::Attribute("f".to_string())])), - op: CompareOp::Eq, - right: Box::new(Expr::Placeholder(":b".to_string())), - }), - ); - let filter = condition_to_filter(&expr, &maps).unwrap(); - let expected = doc! { - "$or": [ - { "item_data.f.S": "x" }, - { "item_data.f.S": "y" } - ] - }; - assert_eq!(filter, expected); - } - - #[test] - fn test_not_condition() { - let maps = make_maps(vec![], vec![]); - let expr = Expr::Not(Box::new(Expr::Function { - name: "attribute_exists".to_string(), - args: vec![Expr::Path(vec![PathElement::Attribute( - "deleted".to_string(), - )])], - })); - let filter = condition_to_filter(&expr, &maps).unwrap(); - let expected = doc! { - "$nor": [{ "item_data.deleted": { "$exists": true } }] - }; - assert_eq!(filter, expected); - } - - #[test] - fn test_begins_with() { - let maps = make_maps( - vec![], - vec![(":prefix", AttributeValue::S("user#".to_string()))], - ); - let expr = Expr::Function { - name: "begins_with".to_string(), - args: vec![ - Expr::Path(vec![PathElement::Attribute("sk".to_string())]), - Expr::Placeholder(":prefix".to_string()), - ], - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!(filter, doc! { "item_data.sk.S": { "$regex": "^user#" } }); - } - - #[test] - fn test_between() { - let maps = make_maps( - vec![], - vec![ - (":lo", AttributeValue::N("10".to_string())), - (":hi", AttributeValue::N("20".to_string())), - ], - ); - let expr = Expr::Between { - operand: Box::new(Expr::Path(vec![PathElement::Attribute("age".to_string())])), - low: Box::new(Expr::Placeholder(":lo".to_string())), - high: Box::new(Expr::Placeholder(":hi".to_string())), - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!( - filter, - doc! { "item_data.age.N": { "$gte": "10", "$lte": "20" } } - ); - } - - #[test] - fn test_in_condition() { - let maps = make_maps( - vec![], - vec![ - (":v1", AttributeValue::S("a".to_string())), - (":v2", AttributeValue::S("b".to_string())), - (":v3", AttributeValue::S("c".to_string())), - ], - ); - let expr = Expr::In { - operand: Box::new(Expr::Path(vec![PathElement::Attribute("x".to_string())])), - list: vec![ - Expr::Placeholder(":v1".to_string()), - Expr::Placeholder(":v2".to_string()), - Expr::Placeholder(":v3".to_string()), - ], - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!(filter, doc! { "item_data.x.S": { "$in": ["a", "b", "c"] } }); - } - - #[test] - fn test_name_ref_resolution() { - let maps = make_maps( - vec![("n", "status")], - vec![(":v", AttributeValue::S("active".to_string()))], - ); - let expr = Expr::Compare { - left: Box::new(Expr::Path(vec![PathElement::Attribute("#n".to_string())])), - op: CompareOp::Eq, - right: Box::new(Expr::Placeholder(":v".to_string())), - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!(filter, doc! { "item_data.status.S": "active" }); - } - - #[test] - fn test_ne_comparison() { - let maps = make_maps( - vec![], - vec![(":v", AttributeValue::S("deleted".to_string()))], - ); - let expr = Expr::Compare { - left: Box::new(Expr::Path(vec![PathElement::Attribute( - "status".to_string(), - )])), - op: CompareOp::Ne, - right: Box::new(Expr::Placeholder(":v".to_string())), - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!(filter, doc! { "item_data.status.S": { "$ne": "deleted" } }); - } - - #[test] - fn test_regex_escape() { - assert_eq!(regex_escape("user.name"), "user\\.name"); - assert_eq!(regex_escape("a+b"), "a\\+b"); - assert_eq!(regex_escape("normal"), "normal"); - } - - #[test] - fn test_attribute_type() { - let maps = make_maps(vec![], vec![(":t", AttributeValue::S("S".to_string()))]); - let expr = Expr::Function { - name: "attribute_type".to_string(), - args: vec![ - Expr::Path(vec![PathElement::Attribute("field".to_string())]), - Expr::Placeholder(":t".to_string()), - ], - }; - let filter = condition_to_filter(&expr, &maps).unwrap(); - assert_eq!(filter, doc! { "item_data.field.S": { "$exists": true } }); - } -} +#[path = "condition_tests.rs"] +mod tests; diff --git a/crates/storage-mongodb/src/condition_tests.rs b/crates/storage-mongodb/src/condition_tests.rs new file mode 100644 index 000000000..cc19ac3e3 --- /dev/null +++ b/crates/storage-mongodb/src/condition_tests.rs @@ -0,0 +1,265 @@ +// Copyright 2026 ExtendDB contributors +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use extenddb_core::expression::ExpressionMaps; +use extenddb_core::types::AttributeValue; +use std::collections::HashMap; + +fn make_maps(names: Vec<(&str, &str)>, values: Vec<(&str, AttributeValue)>) -> ExpressionMaps { + let names_map: HashMap = names + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let values_map: HashMap = values + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(); + ExpressionMaps::new(names_map, values_map) +} + +#[test] +fn test_attribute_exists() { + let maps = make_maps(vec![], vec![]); + let expr = Expr::Function { + name: "attribute_exists".to_string(), + args: vec![Expr::Path(vec![PathElement::Attribute("foo".to_string())])], + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.foo": { "$exists": true } }); +} + +#[test] +fn test_attribute_not_exists() { + let maps = make_maps(vec![], vec![]); + let expr = Expr::Function { + name: "attribute_not_exists".to_string(), + args: vec![Expr::Path(vec![PathElement::Attribute("bar".to_string())])], + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.bar": { "$exists": false } }); +} + +#[test] +fn test_equality_comparison_string() { + let maps = make_maps( + vec![], + vec![(":val", AttributeValue::S("hello".to_string()))], + ); + let expr = Expr::Compare { + left: Box::new(Expr::Path(vec![PathElement::Attribute("name".to_string())])), + op: CompareOp::Eq, + right: Box::new(Expr::Placeholder(":val".to_string())), + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.name.S": "hello" }); +} + +#[test] +fn test_less_than_comparison_number() { + let maps = make_maps(vec![], vec![(":min", AttributeValue::N("100".to_string()))]); + let expr = Expr::Compare { + left: Box::new(Expr::Path(vec![PathElement::Attribute( + "price".to_string(), + )])), + op: CompareOp::Lt, + right: Box::new(Expr::Placeholder(":min".to_string())), + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.price.N": { "$lt": "100" } }); +} + +#[test] +fn test_and_condition() { + let maps = make_maps( + vec![], + vec![ + (":v1", AttributeValue::S("active".to_string())), + (":v2", AttributeValue::N("5".to_string())), + ], + ); + let expr = Expr::And( + Box::new(Expr::Compare { + left: Box::new(Expr::Path(vec![PathElement::Attribute( + "status".to_string(), + )])), + op: CompareOp::Eq, + right: Box::new(Expr::Placeholder(":v1".to_string())), + }), + Box::new(Expr::Compare { + left: Box::new(Expr::Path(vec![PathElement::Attribute( + "count".to_string(), + )])), + op: CompareOp::Gt, + right: Box::new(Expr::Placeholder(":v2".to_string())), + }), + ); + let filter = condition_to_filter(&expr, &maps).unwrap(); + let expected = doc! { + "$and": [ + { "item_data.status.S": "active" }, + { "item_data.count.N": { "$gt": "5" } } + ] + }; + assert_eq!(filter, expected); +} + +#[test] +fn test_or_condition() { + let maps = make_maps( + vec![], + vec![ + (":a", AttributeValue::S("x".to_string())), + (":b", AttributeValue::S("y".to_string())), + ], + ); + let expr = Expr::Or( + Box::new(Expr::Compare { + left: Box::new(Expr::Path(vec![PathElement::Attribute("f".to_string())])), + op: CompareOp::Eq, + right: Box::new(Expr::Placeholder(":a".to_string())), + }), + Box::new(Expr::Compare { + left: Box::new(Expr::Path(vec![PathElement::Attribute("f".to_string())])), + op: CompareOp::Eq, + right: Box::new(Expr::Placeholder(":b".to_string())), + }), + ); + let filter = condition_to_filter(&expr, &maps).unwrap(); + let expected = doc! { + "$or": [ + { "item_data.f.S": "x" }, + { "item_data.f.S": "y" } + ] + }; + assert_eq!(filter, expected); +} + +#[test] +fn test_not_condition() { + let maps = make_maps(vec![], vec![]); + let expr = Expr::Not(Box::new(Expr::Function { + name: "attribute_exists".to_string(), + args: vec![Expr::Path(vec![PathElement::Attribute( + "deleted".to_string(), + )])], + })); + let filter = condition_to_filter(&expr, &maps).unwrap(); + let expected = doc! { + "$nor": [{ "item_data.deleted": { "$exists": true } }] + }; + assert_eq!(filter, expected); +} + +#[test] +fn test_begins_with() { + let maps = make_maps( + vec![], + vec![(":prefix", AttributeValue::S("user#".to_string()))], + ); + let expr = Expr::Function { + name: "begins_with".to_string(), + args: vec![ + Expr::Path(vec![PathElement::Attribute("sk".to_string())]), + Expr::Placeholder(":prefix".to_string()), + ], + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.sk.S": { "$regex": "^user#" } }); +} + +#[test] +fn test_between() { + let maps = make_maps( + vec![], + vec![ + (":lo", AttributeValue::N("10".to_string())), + (":hi", AttributeValue::N("20".to_string())), + ], + ); + let expr = Expr::Between { + operand: Box::new(Expr::Path(vec![PathElement::Attribute("age".to_string())])), + low: Box::new(Expr::Placeholder(":lo".to_string())), + high: Box::new(Expr::Placeholder(":hi".to_string())), + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!( + filter, + doc! { "item_data.age.N": { "$gte": "10", "$lte": "20" } } + ); +} + +#[test] +fn test_in_condition() { + let maps = make_maps( + vec![], + vec![ + (":v1", AttributeValue::S("a".to_string())), + (":v2", AttributeValue::S("b".to_string())), + (":v3", AttributeValue::S("c".to_string())), + ], + ); + let expr = Expr::In { + operand: Box::new(Expr::Path(vec![PathElement::Attribute("x".to_string())])), + list: vec![ + Expr::Placeholder(":v1".to_string()), + Expr::Placeholder(":v2".to_string()), + Expr::Placeholder(":v3".to_string()), + ], + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.x.S": { "$in": ["a", "b", "c"] } }); +} + +#[test] +fn test_name_ref_resolution() { + let maps = make_maps( + vec![("n", "status")], + vec![(":v", AttributeValue::S("active".to_string()))], + ); + let expr = Expr::Compare { + left: Box::new(Expr::Path(vec![PathElement::Attribute("#n".to_string())])), + op: CompareOp::Eq, + right: Box::new(Expr::Placeholder(":v".to_string())), + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.status.S": "active" }); +} + +#[test] +fn test_ne_comparison() { + let maps = make_maps( + vec![], + vec![(":v", AttributeValue::S("deleted".to_string()))], + ); + let expr = Expr::Compare { + left: Box::new(Expr::Path(vec![PathElement::Attribute( + "status".to_string(), + )])), + op: CompareOp::Ne, + right: Box::new(Expr::Placeholder(":v".to_string())), + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.status.S": { "$ne": "deleted" } }); +} + +#[test] +fn test_regex_escape() { + assert_eq!(regex_escape("user.name"), "user\\.name"); + assert_eq!(regex_escape("a+b"), "a\\+b"); + assert_eq!(regex_escape("normal"), "normal"); +} + +#[test] +fn test_attribute_type() { + let maps = make_maps(vec![], vec![(":t", AttributeValue::S("S".to_string()))]); + let expr = Expr::Function { + name: "attribute_type".to_string(), + args: vec![ + Expr::Path(vec![PathElement::Attribute("field".to_string())]), + Expr::Placeholder(":t".to_string()), + ], + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.field.S": { "$exists": true } }); +} From 4dd9bab67f64fed0e7c10e1e4b8fccd5b931a3dd Mon Sep 17 00:00:00 2001 From: Lee Hannigan Date: Sun, 25 Oct 2026 20:55:08 +0000 Subject: [PATCH 2/3] fix(mongodb): reach map members under their M field in a pushed-down condition On a table with no index and no stream, while the server's per-process cache holds a fresh entry recording that the table has no index, a pushable UpdateItem or DeleteItem condition is compiled to a MongoDB filter. The compiler turned `#m.#k` into `item_data.m.k`, but each map's members are stored under its `M` field (`item_data.m.M.k`), so a condition on a map member never matched: `attribute_exists(#m.#k)` and `#m.#k = :v` answered ConditionalCheckFailedException when they held, and `attribute_not_exists` and `<>` on a present member let the write through when they failed. A member name after the top-level attribute now gets an `M` segment, as a list index already got an `L` segment. Amazon DynamoDB evaluates these conditions as expected on map members, list elements inside maps and map members inside list elements (measured 2026-10-25 at 20:51 and 20:52 UTC, each request twice). --- crates/storage-mongodb/src/condition.rs | 14 ++++- crates/storage-mongodb/src/condition_tests.rs | 63 +++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/crates/storage-mongodb/src/condition.rs b/crates/storage-mongodb/src/condition.rs index 3a54744e4..d3bab9a81 100644 --- a/crates/storage-mongodb/src/condition.rs +++ b/crates/storage-mongodb/src/condition.rs @@ -22,14 +22,17 @@ pub fn condition_to_filter(expr: &Expr, maps: &ExpressionMaps) -> Result Result { let mut parts = vec!["item_data".to_string()]; - for elem in elements { + for (position, elem) in elements.iter().enumerate() { match elem { PathElement::Attribute(name) => { let resolved = if let Some(stripped) = name.strip_prefix('#') { @@ -39,6 +42,11 @@ fn resolve_path_to_field( } else { name.clone() }; + // A member name after the top-level attribute is a key of + // the enclosing map. + if position > 0 { + parts.push("M".to_string()); + } parts.push(resolved); } PathElement::Index(idx) => { diff --git a/crates/storage-mongodb/src/condition_tests.rs b/crates/storage-mongodb/src/condition_tests.rs index cc19ac3e3..7ea48dace 100644 --- a/crates/storage-mongodb/src/condition_tests.rs +++ b/crates/storage-mongodb/src/condition_tests.rs @@ -263,3 +263,66 @@ fn test_attribute_type() { let filter = condition_to_filter(&expr, &maps).unwrap(); assert_eq!(filter, doc! { "item_data.field.S": { "$exists": true } }); } + +fn path(elements: &[&str]) -> Expr { + Expr::Path( + elements + .iter() + .map(|e| match e.parse::() { + Ok(i) => PathElement::Index(i), + Err(_) => PathElement::Attribute((*e).to_string()), + }) + .collect(), + ) +} + +fn exists(elements: &[&str]) -> Document { + let maps = make_maps(vec![("m", "m"), ("k", "k")], vec![]); + let expr = Expr::Function { + name: "attribute_exists".to_string(), + args: vec![path(elements)], + }; + condition_to_filter(&expr, &maps).unwrap() +} + +#[test] +fn map_members_and_list_elements_use_the_typed_fields() { + // A map member sits under the map's `M` field and a list element under + // the list's `L` field, as `item_to_document` stores them. + assert_eq!( + exists(&["#m", "#k"]), + doc! { "item_data.m.M.k": { "$exists": true } } + ); + assert_eq!( + exists(&["m", "mm", "k"]), + doc! { "item_data.m.M.mm.M.k": { "$exists": true } } + ); + assert_eq!( + exists(&["l", "0", "k"]), + doc! { "item_data.l.L.0.M.k": { "$exists": true } } + ); + assert_eq!( + exists(&["m", "n", "1"]), + doc! { "item_data.m.M.n.L.1": { "$exists": true } } + ); + assert_eq!( + exists(&["l", "0"]), + doc! { "item_data.l.L.0": { "$exists": true } } + ); + assert_eq!(exists(&["m"]), doc! { "item_data.m": { "$exists": true } }); +} + +#[test] +fn comparison_on_a_map_member_uses_the_typed_field() { + let maps = make_maps( + vec![("m", "m"), ("k", "k")], + vec![(":v", AttributeValue::S("inner".to_string()))], + ); + let expr = Expr::Compare { + left: Box::new(path(&["#m", "#k"])), + op: CompareOp::Eq, + right: Box::new(Expr::Placeholder(":v".to_string())), + }; + let filter = condition_to_filter(&expr, &maps).unwrap(); + assert_eq!(filter, doc! { "item_data.m.M.k.S": "inner" }); +} From cf24c20481f37e24aea38d89ff0ce0c7ce3f0c5e Mon Sep 17 00:00:00 2001 From: Lee Hannigan Date: Sun, 25 Oct 2026 20:55:40 +0000 Subject: [PATCH 3/3] test(mongodb): conditions on nested map members and list elements Dual-target wire tests from measurements on 2026-10-25 at 20:51 and 20:52 UTC: attribute_exists, attribute_not_exists, comparisons, begins_with, contains and attribute_type on map members, list elements inside maps and map members inside list elements, on UpdateItem and DeleteItem, holding and failing. Each test puts its own item right before the conditional write, so on MongoDB the write runs while the cache entry that enables the pushdown is fresh. --- tests/test_condition_nested_paths.py | 161 +++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/test_condition_nested_paths.py diff --git a/tests/test_condition_nested_paths.py b/tests/test_condition_nested_paths.py new file mode 100644 index 000000000..682f83f70 --- /dev/null +++ b/tests/test_condition_nested_paths.py @@ -0,0 +1,161 @@ +# Copyright 2026 ExtendDB contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Conditions on nested map members and list elements, on UpdateItem and DeleteItem. + +The item holds ``m = {k: "inner", n: [10, "b"], mm: {k: "deep"}}`` and +``l = [{k: "first"}, "s"]``. Every expected answer was measured against Amazon +DynamoDB on 2026-10-25 at 20:51 and 20:52 UTC (us-east-1), each request twice: +a holding condition applies the write and a failing one answers +ConditionalCheckFailedException. + +On MongoDB, a condition on a table with no index and no stream is compiled to +a MongoDB filter while the server's cache records that the table has no index. +That entry is written by a session-path write and lasts 60 seconds; each test +puts its own item immediately before the conditional write, and that put +renews the entry when it has expired. + +Dual-target against Amazon DynamoDB and extenddb. +""" + +from __future__ import annotations + +import re +import uuid + +import pytest +from botocore.exceptions import ClientError + +from conftest import scoped_table + +NAMES = {"#m": "m", "#k": "k", "#n": "n", "#mm": "mm", "#l": "l", "#z": "z", "#u": "u"} +VALUES = { + ":x": {"S": "z"}, + ":inner": {"S": "inner"}, + ":deep": {"S": "deep"}, + ":first": {"S": "first"}, + ":b": {"S": "b"}, + ":in": {"S": "in"}, + ":ner": {"S": "ner"}, + ":S": {"S": "S"}, + ":M": {"S": "M"}, + ":other": {"S": "other"}, + ":s": {"S": "s"}, +} + +ITEM = { + "v": {"S": "x"}, + "m": { + "M": { + "k": {"S": "inner"}, + "n": {"L": [{"N": "10"}, {"S": "b"}]}, + "mm": {"M": {"k": {"S": "deep"}}}, + } + }, + "l": {"L": [{"M": {"k": {"S": "first"}}}, {"S": "s"}]}, +} + +HOLDS = [ + "attribute_exists(#m.#k)", + "attribute_not_exists(#m.#z)", + "#m.#k = :inner", + "begins_with(#m.#k, :in)", + "contains(#m.#k, :ner)", + "attribute_type(#m.#k, :S)", + "attribute_type(#m.#mm, :M)", + "#m.#mm.#k = :deep", + "#l[0].#k = :first", + "#m.#n[1] = :b", + "#l[1] = :s", + "attribute_exists(#m.#k) AND #m.#mm.#k = :deep AND #l[0].#k = :first", + "NOT attribute_exists(#m.#mm.#z)", +] + +FAILS = [ + "attribute_exists(#m.#z)", + "attribute_not_exists(#m.#k)", + "#m.#k <> :inner", + "attribute_exists(#l.#k)", + "attribute_exists(#m[0])", + "#m.#k = :other OR #m.#mm.#k = :other OR #l[0].#k = :other", +] + + +def _maps(*exprs: str) -> dict: + tokens = set(re.findall(r"[#:][A-Za-z0-9_]+", " ".join(exprs))) + out = {"ExpressionAttributeNames": {k: v for k, v in NAMES.items() if k in tokens}} + values = {k: v for k, v in VALUES.items() if k in tokens} + if values: + out["ExpressionAttributeValues"] = values + return out + + +@pytest.fixture(scope="module") +def table(dynamodb_client): + with scoped_table( + dynamodb_client, + attribute_definitions=[ + {"AttributeName": "pk", "AttributeType": "S"}, + {"AttributeName": "sk", "AttributeType": "S"}, + ], + key_schema=[ + {"AttributeName": "pk", "KeyType": "HASH"}, + {"AttributeName": "sk", "KeyType": "RANGE"}, + ], + ) as name: + yield name + + +@pytest.fixture +def key(dynamodb_client, table): + key = {"pk": {"S": f"nested-{uuid.uuid4().hex[:8]}"}, "sk": {"S": "item"}} + dynamodb_client.put_item(TableName=table, Item={**key, **ITEM}) + return key + + +def _get(client, table, key): + return client.get_item(TableName=table, Key=key, ConsistentRead=True).get("Item") + + +def _update(client, table, key, condition): + return client.update_item( + TableName=table, + Key=key, + UpdateExpression="SET #u = :x", + ConditionExpression=condition, + **_maps("SET #u = :x", condition), + ) + + +@pytest.mark.parametrize("condition", HOLDS) +def test_update_with_holding_nested_condition_applies(dynamodb_client, table, key, condition): + _update(dynamodb_client, table, key, condition) + assert _get(dynamodb_client, table, key)["u"] == {"S": "z"} + + +@pytest.mark.parametrize("condition", FAILS) +def test_update_with_failing_nested_condition_is_refused(dynamodb_client, table, key, condition): + with pytest.raises(ClientError) as exc_info: + _update(dynamodb_client, table, key, condition) + err = exc_info.value.response["Error"] + assert err["Code"] == "ConditionalCheckFailedException", err + assert "u" not in _get(dynamodb_client, table, key) + + +def test_delete_with_holding_nested_condition_applies(dynamodb_client, table, key): + condition = "#m.#mm.#k = :deep AND #l[0].#k = :first" + dynamodb_client.delete_item( + TableName=table, Key=key, ConditionExpression=condition, **_maps(condition) + ) + assert _get(dynamodb_client, table, key) is None + + +def test_delete_with_failing_nested_condition_is_refused(dynamodb_client, table, key): + condition = "#m.#mm.#k = :other" + with pytest.raises(ClientError) as exc_info: + dynamodb_client.delete_item( + TableName=table, Key=key, ConditionExpression=condition, **_maps(condition) + ) + err = exc_info.value.response["Error"] + assert err["Code"] == "ConditionalCheckFailedException", err + assert _get(dynamodb_client, table, key) is not None