Repository navigation
fix(update): write nothing for a removal-only UpdateItem on a missing key - #508
Open
LeeroyHannigan wants to merge 4 commits into
Open
LeeroyHannigan wants to merge 4 commits into
LeeroyHannigan wants to merge 4 commits into
Conversation
UpdateItem on a key that holds no item used to create a key-only item whenever the update expression consisted solely of REMOVE or DELETE actions, and the legacy AttributeUpdates form with only DELETE actions did the same. Amazon DynamoDB writes nothing for such a request: no item is created, no stream record is emitted, and the response carries no Attributes under any ReturnValues, while ConsumedCapacity still reports one write unit. SET, ADD and an update with no actions at all still create the item. The decision lives in one place, extenddb_core::expression::removes_only, and every backend applies it after the actions have run against the key-only image, so validation errors are raised exactly as before. SQLite, PostgreSQL and Cassandra return before their insert; MongoDB runs the native fast path without upsert, ends the transaction path with a NoWrite outcome, and returns from the pushdown path before replace_one. Fixes #478
In-memory backend tests for the no-write decision: REMOVE, DELETE and their combinations on a missing key return no image and create no item; SET, ADD, a mixed expression and an update with no actions still create the item; REMOVE on an existing item still removes the attribute.
The removal-only tests now also run on a table with a GSI and an LSI: no item and no index entry appears, ItemCollectionMetrics comes from the key, and ReturnConsumedCapacity INDEXES charges the table only, as the service answers. Two tests write to the table before the removal-only update, so a backend that serves later updates on a table through another path is covered there too. REMOVE plus ADD and DELETE plus ADD on a missing key still create the item with the added number, as measured on the service.
LeeroyHannigan
requested review from
amrith,
c33howard,
jcshepherd,
pdf-amzn,
robinnsc and
yesyayen
as code owners
October 24, 2026 13:04
The live test of an UpdateItem that creates an item had a REMOVE case that expected the update to create a key-only document. With removal-only updates on a missing key writing nothing, that case is now its own live test: no old or new image, no document, and a PutItem on the key still creates the item. The test table records that it has no GSI, so the update takes the native fast path.
This was referenced Oct 24, 2026
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.
Stacked on #473 (
fix/mongodb-update-creates-key): the branch starts at its head, 4e6ccc9, and the PR targets that branch, so MongoDB runs this change with #473's key fix underneath. GitHub retargets it to main when #473 merges.Summary
UpdateItem on a key that holds no item created a key-only item whenever the update only removed attributes (
REMOVE,DELETE, or the legacy AttributeUpdates form with onlyDELETEactions). Amazon DynamoDB writes nothing for such a request. This change makes all four backends write nothing in that case.Fixes #478
What changed
extenddb_core::expression::removes_only(actions)is the shared decision: true when the action list is non-empty and every action isREMOVEorDELETE. An update with no actions at all is not covered, because the service does create a key-only item for it.removes_onlyholds, returning no old and no new image.crates/storage-sqlite/src/data/update_item.rs): return beforeupsert_item_in_tx.crates/storage-postgres/src/data/update_item.rs): return before theINSERT ... ON CONFLICT DO NOTHING.crates/storage-cassandra/src/data/update_item.rs): return before the TTL claim and the LWT write.crates/storage-mongodb/src/data_engine.rs): the native fast path runsfindAndModifywithupsert: falsefor removal-only updates and answers from the result; the transaction path ends in a newNoWriteoutcome that aborts the read-only transaction; the pushdown path returns beforereplace_one.DataEngine::update_itemcontract incrates/storage/src/lib.rsdocuments the exception.crates/engine/src/update_item.rs) is unchanged: with no new image it already omitsAttributesfor everyReturnValues, charges one write unit, and derivesItemCollectionMetricsfrom the key, which is what the service returns.Service measurements
us-east-1, client retries off. Tables: bb-r2-idx (pk S, a GSI on
g1pk, no LSI, stream NEW_AND_OLD_IMAGES) and bb-arn-main (pk S, sk S, a GSIgsi1ong1pkand an LSIlsi1onl1sk, stream on). Every row below was measured on both tables with the same result, except the rows marked bb-arn-main: ItemCollectionMetrics needs an LSI, which only bb-arn-main has. Fresh key per case; "no item" means GetItem with ConsistentRead returned no Item afterwards.REMOVE a, ReturnValues NONE, ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATED_NEW{}(no Attributes for any of them)REMOVE a, b, ReturnValues NONE, ALL_NEW, UPDATED_NEW{}REMOVE a.b[0]{}DELETE s :v(SS), ReturnValues NONE and ALL_NEW{}REMOVE a DELETE s :v, ALL_NEW{}REMOVE awithattribute_not_exists(pk), ALL_NEW{}REMOVE awithattribute_exists(pk){"a": {"Action": "DELETE"}}, NONE and ALL_NEW{}{}{"s": {"Action": "DELETE", "Value": {"SS": ["x"]}}}, NONE and ALL_NEW{}REMOVE a, ReturnConsumedCapacity TOTALConsumedCapacity: {CapacityUnits: 1.0, TableName}REMOVE a, ReturnConsumedCapacity INDEXES{CapacityUnits: 1.0, Table: {CapacityUnits: 1.0}}REMOVE a, ReturnItemCollectionMetrics SIZE (bb-arn-main)[0.0, 1.0]REMOVE g1pk, l1sk, ALL_NEW (bb-arn-main){}; queries ongsi1andlsi1return nothing for the keyREMOVE g1pk, ReturnConsumedCapacity INDEXES (bb-arn-main){CapacityUnits: 1.0, Table: {CapacityUnits: 1.0}}, no GlobalSecondaryIndexes member; theSET g1pk = :vcontrol charges 2.0 withgsi11.0REMOVE a ADD n :v,DELETE s :s ADD n :nandADD n :v REMOVE a, ALL_NEW (bb-arn-main, twice each){pk, sk, n: 1}{pk, sk, n: 1}createdREMOVE aon a stream-enabled table, LATEST iterators drained for 10 sSET a = :v;ADD n :v;REMOVE a SET b = :v; AttributeUpdates PUT; AttributeUpdates DELETE plus PUT{}(no actions){}{}{pk, a, b}:REMOVE athenREMOVE b, ALL_NEW{pk, b}then{pk}Tests
tests/test_update_remove_only_missing_item.py(new, dual-target): 64 tests over a hash table, a hash and range table, and a hash and range table with a GSI and an LSI.REMOVEplus SET,REMOVEplus ADD,DELETEplus ADD, legacy DELETE plus PUT, AttributeUpdates{}, and REMOVE on an existing item.crates/core/src/expression/ast.rs: unit tests forremoves_only.crates/storage-sqlite/src/data/update_item_tests.rs: in-memory backend tests for the no-write decision, the SET, ADD, mixed and no-action controls, and REMOVE on an existing item.crates/storage-mongodb/src/data_engine_live_tests.rs: fix(mongodb): store the key when UpdateItem creates an item #473's live test of an UpdateItem that creates an item had a REMOVE case that expected a key-only document. That case is now its own live test,live_remove_only_update_on_a_missing_key_writes_nothing: no image, no document, and a PutItem on the key still creates the item. The MongoDB live tests pass 8 of 8 against a replica set.crates/storage-mongodb/src/data_engine.rs(4,475 lines) andcrates/storage/src/lib.rs(1,028 lines) were over 600 lines before this change, which adds 44 and 4 lines to them; they are not split here.Interactions with open PRs
fix/mongodb-update-creates-key): this PR's base. fix(mongodb): store the key when UpdateItem creates an item #473 makes the MongoDB native upsert store the key when UpdateItem creates an item, and repairs stored documents that have no item data into key-only items. This change removes one source of such documents: before it, a REMOVE-only UpdateItem on a missing key created a document with no item data on MongoDB (the 500 in MongoDB: UpdateItem that creates an item stores it without its key attribute, and every later PutItem on that key answers 500 #436). Documents created that way before both changes land are what fix(mongodb): store the key when UpdateItem creates an item #473's repair turns into key-only items; after this change no new ones are created by REMOVE or DELETE, and the repair stays correct for the ones that exist. The native fast path keeps fix(mongodb): store the key when UpdateItem creates an item #473's$setOnInsertfor SET and ADD and usesupsert: falsefor removal-only updates. On main alone, MongoDB answers the mixed controls (REMOVE a SET b = :v, legacy DELETE plus PUT) without the key attributes, which is MongoDB: UpdateItem that creates an item stores it without its key attribute, and every later PutItem on that key answers 500 #436 and the reason this PR sits on fix(mongodb): store the key when UpdateItem creates an item #473.fix/issue-410-update-item-500) and fix(postgres): build a GSI added by UpdateTable correctly while writes continue #401 (fix/pg-online-gsi-backfill): both insert a secondary-index key check after the item-size validation in the Cassandra and PostgreSQLupdate_item.rs, where this change inserts its early return; git reports an adjacent-insertion conflict. Resolution: keep both, their check first, then the early return. The order does not change any answer: for a removal-only update on a missing key the new image is the key alone, so the index-key checks have nothing to refuse.fix/transact-validation-order): both edit thepub uselines incrates/core/src/expression/mod.rs. Resolution: keep both exports (removes_onlyfromast,compare_orderablefromevaluator).Not changed here
REMOVEorDELETEon a missing key still creates a key-only item on every backend. The service writes nothing for it (measured 2026-10-24 08:49:02 UTC,REMOVE a,REMOVE a, b,DELETE s :v, andREMOVE awithattribute_not_exists(pk), both tables, ConsumedCapacity 2.0 write units like the SET control). The transaction update path is separate code in each backend, so it is a separate change. No issue is filed for it yet.REMOVE a SET a = :von the service is rejected with "Two document paths overlap with each other; must remove or rewrite one of these paths; path one: [a], path two: [a]" (measured 2026-10-24 08:49:02 UTC). Not checked against ExtendDB here.