Repository navigation
fix(mongodb): evaluate deeply nested conditions instead of answering 500 - #571
Open
LeeroyHannigan wants to merge 3 commits into
Open
LeeroyHannigan wants to merge 3 commits into
LeeroyHannigan wants to merge 3 commits into
Conversation
pushdown.rs was 635 lines. The test module moves unchanged to pushdown_tests.rs, loaded with #[path] as backup_engine.rs loads its test files, so the next change can add to the analyzer without the file passing 600 lines.
…ushing it down On a table with no index and no stream, while the server's per-process cache holds a fresh entry (60 seconds) recording that the table has no index, a pushable UpdateItem or DeleteItem condition is compiled to a MongoDB filter, one `$and`, `$or` or `$nor` level per operator. MongoDB refuses a command nested past its BSON depth limit, so a chain of about 100 terms (`#v = :v OR #v = :v OR ...`) answered InternalServerError, and only while that cache entry was fresh. Amazon DynamoDB evaluates such conditions: a 150-term OR or AND chain, OR and AND alternating 140 levels deep, and a legacy `Expected` map of 120 entries joined by OR answer ConditionalCheckFailedException when they fail and apply the write when they hold (measured 2026-10-25 at 18:34 and 19:53 UTC, each failing case twice). The analyzer now marks a condition whose AND, OR and NOT nest more than 64 levels deep as not pushable, so it takes the session path that evaluates the condition in Rust. Measured on mongo:7.0.45 through the calls the pushdown makes, with the key filter merged in, a condition nested 98 levels runs and 99 fails; with `contains` leaves, 97 runs and 98 fails.
…leteItem Dual-target wire tests from measurements on 2026-10-25 at 18:34 and 19:53 UTC: a 150-term OR or AND chain, OR and AND alternating 100 and 140 levels deep, a 120-term OR inside an AND, and a legacy `Expected` map of 120 entries joined by OR answer ConditionalCheckFailedException when they fail on an absent key, and UpdateItem and DeleteItem apply when such a condition holds. Each test creates its own table and writes to it first, so on MongoDB the conditional write runs while the cache entry that enables the pushdown is fresh. The UpdateItem row in test_expression_operator_limits.py no longer needs its expected-failure marker on MongoDB.
LeeroyHannigan
requested review from
amrith,
c33howard,
jcshepherd,
pdf-amzn,
robinnsc and
yesyayen
as code owners
October 25, 2026 20:16
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On MongoDB, an UpdateItem or DeleteItem condition with about 100 nested AND or OR terms answered InternalServerError. It is now evaluated, and the request answers as DynamoDB does.
This branch is stacked on #558, because it removes the expected-failure marker #558 put on its 150-term UpdateItem condition row for MongoDB.
What happened
On a table with no index and no stream, the MongoDB backend has a fast path for conditional writes. It compiles the condition into a MongoDB filter and sends it with the key filter, through
find_onefor UpdateItem andfind_one_and_deletefor DeleteItem. The compiler emits one$and,$oror$norlevel per operator, and a chain such as#v = :v OR #v = :v OR ...parses left-nested, so 150 terms become 149 nested levels. MongoDB refuses a command nested past its BSON depth limit with error 15 ("BSONObj exceeds maximum nested object depth"), and the server answered InternalServerError.The fast path runs only while the server's per-process cache holds an entry recording that the table has no index. A write on the session path creates that entry, and it lasts 60 seconds. Outside that window the same request took the session path and answered correctly.
Measured on mongo:7.0.45 through those two calls, with the key filter merged in, a condition nested 98 levels runs and 99 fails. With
containsleaves, the deepest leaf the compiler emits, 97 runs and 98 fails. Right-nested and alternating chains have the same limits, and list index paths add no nesting.What the service answers
Measured in us-east-1 on 2026-10-25 between 18:34 and 19:55 UTC, each failing case sent twice with the same answer:
Expectedmap of 120 entries joined byConditionalOperatorOR. The same holds with 300 and 301Expectedentries.The change
is_pushablenow refuses a condition whose AND, OR and NOT nest more than 64 levels deep. Such a condition takes the session path, which evaluates it in Rust, as it already does for every condition the analyzer does not accept. At 64, the deepest filter a pushable condition produces is 134 levels against MongoDB's 200. A condition loses the fast path only with 65 or more AND, OR and NOT operators along one path, and the answer is the same on either path.The first commit moves the analyzer's unit tests from
pushdown.rs(635 lines) topushdown_tests.rs, unchanged, loaded with#[path]asbackup_engine.rsloads its test files. The module documentation and section 5.2 ofdocs/design/13-storage-mongodb.mdlist the new condition that is not pushed down.Tests
logical_nesting_past_the_limit_is_not_pushablechecks the boundary at 64 and 65, a 150-term chain, a depth of 97 refused whatever the limit, and that NOT and both sides of an operator count. Four altered versions of the change each fail it: the check removed, the limit raised to 100, NOT not counted, and only the left side counted.tests/test_condition_nesting_depth.py, 17 dual-target tests built from the measured rows above, on UpdateItem and DeleteItem with conditions that fail and conditions that hold, and the legacyExpectedrows. Each test creates its own table and writes to it first, so on MongoDB the conditional write runs while the cache entry that enables the fast path is fresh.tests/test_expression_operator_limits.py: the expected-failure marker ontest_update_item_condition_at_the_cap_is_evaluatedis removed.Run locally on 2026-10-25 against release builds, MongoDB on mongo:7:
test_expression_operator_limits.pyafter the fix: 95 passed on MongoDB, 95 passed on SQLite; an earlier version of the new file, with 11 of the 17 rows and one table for the whole module, passed on PostgreSQL;test_expression_operator_limits.pybefore the fix on MongoDB: the unmarked cap row fails with InternalServerError and the other 77 pass;cargo +1.97.0 fmt --all -- --check,cargo +1.97.0 clippy -p extenddb-storage-mongodb --all-targets -- -D warnings,cargo test -p extenddb-storage-mongodb --lib(123 passed), and the pushdown unit tests on 1.88.0.CI collects the new file in
mongodb-pytest,run-integrationandrun-integration-sqlite.Merging
git merge-tree --write-treeagainst the 13 open PRs that touch the MongoDB analyzer, compiler or data engine is clean for 10. #429 conflicts with #558 inkey_condition.rs, and #416 conflicts withmain; neither conflict involves this branch's files. #368 adds two tests inside the test module that the first commit moves. To resolve it, keep#[path = "pushdown_tests.rs"] mod tests;and move #368'snul_attribute_name_is_not_pushableandcontrol_character_attribute_alias_is_not_pushableintopushdown_tests.rs, one indent level out. The pushdown unit tests pass 24 of 24 on the merged tree.Found while testing, not changed here
These are on
main, filed separately:$together with a numeric ADD (SET #s = :s ADD #n :one,:sis{"S": "$item_data"}) answers InternalServerError, and a later GetItem of that key also answers InternalServerError. The service stores the string (MongoDB: SET of a $-prefixed string with a numeric ADD answers 500 and corrupts the item #573).attribute_exists(#m.#k) AND #m.#k = :v) answers ConditionalCheckFailedException when it holds. The service applies the write (MongoDB: a condition on a nested map member fails when it holds #572).Nesting Levels have exceeded supported limits(UpdateItem: a result nested more than 32 levels is applied; the service refuses it #574).