Skip to content

fix: UpdateItem ignores ConditionExpression on non-existent items#5

Merged
hicksy merged 2 commits into
nubo-db:mainfrom
AnatolyRugalev:fix/update-condition-on-nonexistent-item
Apr 24, 2026
Merged

fix: UpdateItem ignores ConditionExpression on non-existent items#5
hicksy merged 2 commits into
nubo-db:mainfrom
AnatolyRugalev:fix/update-condition-on-nonexistent-item

Conversation

@AnatolyRugalev

Copy link
Copy Markdown
Contributor

Summary

UpdateItem and TransactWriteItems Update with ConditionExpression: attribute_exists(PK) succeed and create a new item when the key does not exist. DynamoDB (and DynamoDB Local) correctly reject with ConditionalCheckFailed.

Version: dynoxide 0.9.8

Reproduction

dynoxide --port 8000 &

export AWS_ACCESS_KEY_ID=fake AWS_SECRET_ACCESS_KEY=fake \
       AWS_DEFAULT_REGION=us-east-1 EP=http://localhost:8000

# Create table
aws dynamodb create-table --endpoint-url $EP \
  --table-name test-condition --billing-mode PAY_PER_REQUEST \
  --attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S \
  --key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE

# TransactWrite Update with attribute_exists on a key that does NOT exist
aws dynamodb transact-write-items --endpoint-url $EP \
  --transact-items '[{
    "Update": {
      "TableName": "test-condition",
      "Key": {"PK": {"S": "does-not-exist"}, "SK": {"S": "nope"}},
      "UpdateExpression": "ADD TagCount :inc",
      "ExpressionAttributeValues": {":inc": {"N": "1"}},
      "ConditionExpression": "attribute_exists(PK)"
    }
  }]'
# => succeeds (should fail)

# Ghost record was created
aws dynamodb get-item --endpoint-url $EP \
  --table-name test-condition \
  --key '{"PK": {"S": "does-not-exist"}, "SK": {"S": "nope"}}'
# => {PK: "does-not-exist", SK: "nope", TagCount: 1}

Expected

TransactionCanceledException with ConditionalCheckFailed. Same for standalone UpdateItem.

Root cause

Both execute_update in transact_write_items.rs and the UpdateItem handler in update_item.rs populate key attributes on the item for the upsert path before evaluating the ConditionExpression. This makes attribute_exists(PK) always pass because PK was just inserted.

Fix

Evaluate the condition against the original existing item (empty for non-existent keys) before populating key attributes. The PR includes regression tests for both code paths.

@hicksy

hicksy commented Apr 22, 2026

Copy link
Copy Markdown
Member

Thanks for this - the root cause writeup makes it easy to review, and good that the tests cover both the UpdateItem and TransactWrite paths. I'll get to it in the next few days.

@hicksy hicksy force-pushed the fix/update-condition-on-nonexistent-item branch from bd18587 to bacf5e5 Compare April 24, 2026 18:01
@hicksy

hicksy commented Apr 24, 2026

Copy link
Copy Markdown
Member

Two small edits pushed to your branch:

  1. Rebased onto current main as our workflows have been updated there recently.
  2. cargo fmt (minor formatting fixes). No logic change.

As you were a first time contributor the CI checks needed approval. CI is approved and running; I'm leaving benchmark-regression unapproved for now (unrelated housekeeping). Will merge once CI is green.

AnatolyRugalev and others added 2 commits April 24, 2026 19:55
Both UpdateItem and TransactWriteItems Update actions populate key
attributes on the item before evaluating the ConditionExpression.
This causes attribute_exists(PK) to always pass, even when the item
does not exist, because PK was just inserted for the upsert path.

Fix: evaluate the condition against the original existing item (empty
for non-existent keys) before populating key attributes.

Regression tests cover both standalone UpdateItem and TransactWriteItems
Update paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hicksy hicksy force-pushed the fix/update-condition-on-nonexistent-item branch from bacf5e5 to 5879105 Compare April 24, 2026 18:58
@hicksy hicksy merged commit 32e6132 into nubo-db:main Apr 24, 2026
6 checks passed
hicksy added a commit to nubo-db/dynamodb-conformance that referenced this pull request Apr 24, 2026
Broadens the single attribute_exists regression test added in #1
with four more variants:

  - attribute_not_exists upserts on non-existent key (canonical
    create-if-absent pattern; previously untested)
  - comparison-style condition rejects on non-existent; no ghost
  - combined attribute_exists + equality rejects on non-existent
  - ReturnValues: ALL_NEW on upsert returns created attributes

All pass real AWS. Dynoxide 0.9.8 (released) fails the two tests
that depend on key-populate-before-condition ordering (see
nubo-db/dynoxide#5); dynoxide main (post-fix) passes all four.
hicksy added a commit to nubo-db/dynamodb-conformance that referenced this pull request Apr 24, 2026
…rage

Mirrors the standalone UpdateItem coverage through the
transactional code path:

  - Update attribute_not_exists upserts on non-existent key
  - Update comparison condition cancels on non-existent; no ghost
  - Update combined attribute_exists + equality cancels; no ghost
  - mixed transaction: one passing, one failing on non-existent
    cancels everything (integration)

All pass real AWS. Dynoxide 0.9.8 (released) fails the two that
depend on key-populate-before-condition ordering (see
nubo-db/dynoxide#5); dynoxide main (post-fix) passes all four.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants