From a1f9cfd065f1a8f4912c709c826d3b9a60e0e097 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 20 Feb 2026 19:33:06 +0200 Subject: [PATCH 01/14] Optimistic Locking for Delete Operations --- .../enhanced/dynamodb/DynamoDbAsyncTable.java | 4 + .../enhanced/dynamodb/DynamoDbTable.java | 4 + .../client/DefaultDynamoDbAsyncTable.java | 32 +- .../internal/client/DefaultDynamoDbTable.java | 30 +- .../model/DeleteItemEnhancedRequest.java | 19 + .../model/OptimisticLockingHelper.java | 145 +++++++ .../TransactDeleteItemEnhancedRequest.java | 18 + .../TransactWriteItemsEnhancedRequest.java | 21 +- .../OptimisticLockingAsyncCrudTest.java | 398 +++++++++++++++++ .../OptimisticLockingCrudTest.java | 394 +++++++++++++++++ .../models/VersionedRecord.java | 122 ++++++ .../model/DeleteItemEnhancedRequestTest.java | 17 + .../model/OptimisticLockingHelperTest.java | 408 ++++++++++++++++++ ...TransactDeleteItemEnhancedRequestTest.java | 10 + 14 files changed, 1614 insertions(+), 8 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/VersionedRecord.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java index e193fe681df8..135302d54b05 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java @@ -247,6 +247,10 @@ default CompletableFuture deleteItem(T keyItem) { throw new UnsupportedOperationException(); } + default CompletableFuture deleteItem(T keyItem, boolean useOptimisticLocking) { + throw new UnsupportedOperationException(); + } + /** * Deletes a single item from the mapped table using a supplied primary {@link Key}. *

diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java index 6e94e6726c2f..49f19a8842dd 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java @@ -245,6 +245,10 @@ default T deleteItem(T keyItem) { throw new UnsupportedOperationException(); } + default T deleteItem(T keyItem, boolean useOptimisticLocking) { + throw new UnsupportedOperationException(); + } + /** * Deletes a single item from the mapped table using a supplied primary {@link Key}. *

diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java index cd281dec3d24..a42d2c5ba13b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.client; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.createKeyFromItem; +import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.conditionallyApplyOptimisticLocking; import java.util.ArrayList; import java.util.concurrent.CompletableFuture; @@ -124,6 +125,9 @@ public CompletableFuture createTable() { .build()); } + /** + * Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}. + */ @Override public CompletableFuture deleteItem(DeleteItemEnhancedRequest request) { TableOperation> operation = DeleteItemOperation.create(request); @@ -131,6 +135,9 @@ public CompletableFuture deleteItem(DeleteItemEnhancedRequest request) { .thenApply(DeleteItemEnhancedResponse::attributes); } + /** + * Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}. + */ @Override public CompletableFuture deleteItem(Consumer requestConsumer) { DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder(); @@ -138,14 +145,35 @@ public CompletableFuture deleteItem(Consumer deleteItem(Key key) { return deleteItem(r -> r.key(key)); } + /** + * @deprecated Use {@link #deleteItem(Object, boolean)} instead to explicitly control optimistic locking behavior. + */ @Override + @Deprecated public CompletableFuture deleteItem(T keyItem) { - return deleteItem(keyFrom(keyItem)); + return deleteItem(keyItem, false); + } + + /** + * Deletes an item from the table with optional optimistic locking. + * + * @param keyItem the item containing the key to delete + * @param useOptimisticLocking if true, applies optimistic locking if the item has version information + * @return a CompletableFuture containing the deleted item, or null if the item was not found + */ + @Override + public CompletableFuture deleteItem(T keyItem, boolean useOptimisticLocking) { + DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)).build(); + request = conditionallyApplyOptimisticLocking(request, keyItem, tableSchema, useOptimisticLocking); + return deleteItem(request); } @Override @@ -311,7 +339,7 @@ public CompletableFuture updateItem(T item) { public Key keyFrom(T item) { return createKeyFromItem(item, tableSchema, TableMetadata.primaryIndexName()); } - + @Override public CompletableFuture deleteTable() { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java index 31ce811b3483..bf219097a444 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.client; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.createKeyFromItem; +import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.conditionallyApplyOptimisticLocking; import java.util.ArrayList; import java.util.function.Consumer; @@ -126,12 +127,18 @@ public void createTable() { .build()); } + /** + * Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}. + */ @Override public T deleteItem(DeleteItemEnhancedRequest request) { TableOperation> operation = DeleteItemOperation.create(request); return operation.executeOnPrimaryIndex(tableSchema, tableName, extension, dynamoDbClient).attributes(); } + /** + * Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}. + */ @Override public T deleteItem(Consumer requestConsumer) { DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder(); @@ -139,14 +146,35 @@ public T deleteItem(Consumer requestConsumer) return deleteItem(builder.build()); } + /** + * Does not support optimistic locking. Use {@link #deleteItem(Object, boolean)} for optimistic locking support. + */ @Override public T deleteItem(Key key) { return deleteItem(r -> r.key(key)); } + /** + * @deprecated Use {@link #deleteItem(Object, boolean)} instead to explicitly control optimistic locking behavior. + */ @Override + @Deprecated public T deleteItem(T keyItem) { - return deleteItem(keyFrom(keyItem)); + return deleteItem(keyItem, false); + } + + /** + * Deletes an item from the table with optional optimistic locking. + * + * @param keyItem the item containing the key to delete + * @param useOptimisticLocking if true, applies optimistic locking if the item has version information + * @return the deleted item, or null if the item was not found + */ + @Override + public T deleteItem(T keyItem, boolean useOptimisticLocking) { + DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)).build(); + request = conditionallyApplyOptimisticLocking(request, keyItem, tableSchema, useOptimisticLocking); + return deleteItem(request); } @Override diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java index 0a7a01500bfd..a27379596331 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.model; +import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.createVersionCondition; + import java.util.Objects; import java.util.function.Consumer; import software.amazon.awssdk.annotations.NotThreadSafe; @@ -24,6 +26,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; @@ -289,6 +292,22 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio return this; } + /** + * Adds optimistic locking to this delete request. + *

+ * This method applies a condition expression that ensures the delete operation only succeeds + * if the version attribute of the item matches the provided expected value. + * + * @param versionValue the expected version value that must match for the deletion to succeed + * @param versionAttributeName the name of the version attribute in the DynamoDB table + * @return a builder of this type with optimistic locking condition applied + * @throws IllegalArgumentException if any parameter is null + */ + public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) { + Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName); + return conditionExpression(optimisticLockingCondition); + } + public DeleteItemEnhancedRequest build() { return new DeleteItemEnhancedRequest(this); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java new file mode 100644 index 000000000000..113af4709ecb --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java @@ -0,0 +1,145 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.model; + +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Utility class for adding optimistic locking to DynamoDB delete operations. + *

+ * Optimistic locking prevents concurrent modifications by checking that an item's version hasn't changed since it was last read. + * If the version has changed, the delete operation fails with a {@code ConditionalCheckFailedException}. + */ +@SdkPublicApi +public final class OptimisticLockingHelper { + + private OptimisticLockingHelper() { + } + + /** + * Adds optimistic locking to a delete request. + * + * @param request the original delete request + * @param versionValue the expected version value + * @param versionAttributeName the version attribute name + * @return delete request with optimistic locking condition + */ + public static DeleteItemEnhancedRequest withOptimisticLocking( + DeleteItemEnhancedRequest request, AttributeValue versionValue, String versionAttributeName) { + + Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName); + return request.toBuilder() + .conditionExpression(conditionExpression) + .build(); + } + + /** + * Adds optimistic locking to a transactional delete request. + * + * @param request the original transactional delete request + * @param versionValue the expected version value + * @param versionAttributeName the version attribute name + * @return transactional delete request with optimistic locking condition + */ + public static TransactDeleteItemEnhancedRequest withOptimisticLocking( + TransactDeleteItemEnhancedRequest request, AttributeValue versionValue, String versionAttributeName) { + + Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName); + return request.toBuilder() + .conditionExpression(conditionExpression) + .build(); + } + + /** + * Conditionally applies optimistic locking if enabled and version information exists. + * + * @param the type of the item + * @param request the original delete request + * @param keyItem the item containing version information + * @param tableSchema the table schema + * @param useOptimisticLocking if true, applies optimistic locking + * @return delete request with optimistic locking if enabled and version exists, otherwise original request + */ + public static DeleteItemEnhancedRequest conditionallyApplyOptimisticLocking( + DeleteItemEnhancedRequest request, T keyItem, TableSchema tableSchema, boolean useOptimisticLocking) { + + if (!useOptimisticLocking) { + return request; + } + + return getVersionAttributeName(tableSchema) + .map(versionAttributeName -> { + AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName); + return version != null ? withOptimisticLocking(request, version, versionAttributeName) : request; + }) + .orElse(request); + } + + /** + * Conditionally applies optimistic locking if enabled and version information exists. + * + * @param the type of the item + * @param request the original transactional delete request + * @param keyItem the item containing version information + * @param tableSchema the table schema + * @param useOptimisticLocking if true, applies optimistic locking + * @return delete request with optimistic locking if enabled and version exists, otherwise original request + */ + public static TransactDeleteItemEnhancedRequest conditionallyApplyOptimisticLocking( + TransactDeleteItemEnhancedRequest request, T keyItem, TableSchema tableSchema, boolean useOptimisticLocking) { + + if (!useOptimisticLocking) { + return request; + } + + return getVersionAttributeName(tableSchema) + .map(versionAttributeName -> { + AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName); + return version != null ? withOptimisticLocking(request, version, versionAttributeName) : request; + }) + .orElse(request); + } + + + /** + * Creates a version condition expression. + * + * @param versionValue the expected version value + * @param versionAttributeName the version attribute name + * @return version check condition expression + */ + public static Expression createVersionCondition(AttributeValue versionValue, String versionAttributeName) { + return Expression.builder() + .expression(versionAttributeName + " = :version_value") + .putExpressionValue(":version_value", versionValue) + .build(); + } + + /** + * Gets the version attribute name from table schema. + * + * @param the type of the item + * @param tableSchema the table schema + * @return version attribute name if present, empty otherwise + */ + public static Optional getVersionAttributeName(TableSchema tableSchema) { + return tableSchema.tableMetadata().customMetadataObject("VersionedRecordExtension:VersionAttribute", String.class); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java index 15c4df8cacd8..fdbad8baa983 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.model; +import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.createVersionCondition; + import java.util.Objects; import java.util.function.Consumer; import software.amazon.awssdk.annotations.NotThreadSafe; @@ -24,6 +26,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; /** @@ -215,6 +218,21 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio return this; } + /** + * Adds optimistic locking to this transactional delete request. + *

+ * This method applies a condition expression that ensures the delete operation only succeeds if the version attribute of + * the item matches the provided expected value. If the condition fails, the entire transaction will be cancelled. + * + * @param versionValue the expected version value that must match for the deletion to succeed + * @param versionAttributeName the name of the version attribute in the DynamoDB table + * @return a builder of this type with optimistic locking condition applied + * @throws IllegalArgumentException if any parameter is null + */ + public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) { + Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName); + return conditionExpression(optimisticLockingCondition); + } public TransactDeleteItemEnhancedRequest build() { return new TransactDeleteItemEnhancedRequest(this); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactWriteItemsEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactWriteItemsEnhancedRequest.java index f322dd67dde2..b6e81f0150fe 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactWriteItemsEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactWriteItemsEnhancedRequest.java @@ -246,6 +246,11 @@ public Builder addDeleteItem(MappedTableResource mappedTableResource, Del * the delete action, see the low-level operation description in for instance * {@link DynamoDbTable#deleteItem(DeleteItemEnhancedRequest)} and how to construct the low-level request in * {@link TransactDeleteItemEnhancedRequest}. + *

+ * For optimistic locking support, use + * {@link TransactDeleteItemEnhancedRequest.Builder#withOptimisticLocking( + * software.amazon.awssdk.services.dynamodb.model.AttributeValue, String)} + * to create a request with version checking conditions before adding it to the transaction. * * @param mappedTableResource the table where the key is located * @param request A {@link TransactDeleteItemEnhancedRequest} @@ -272,13 +277,19 @@ public Builder addDeleteItem(MappedTableResource mappedTableResource, Key } /** - * Adds a primary lookup key for the item to delete, and it's associated table, to the transaction. For more information - * on the delete action, see the low-level operation description in for instance + * Adds the supplied item and its associated table to the transaction for deletion. + *

+ * Unlike {@link #addDeleteItem(MappedTableResource, Key)}, this variant allows you to provide the full modeled item + * instead of only its primary key. + * + * Does not support Optimistic Locking. + *

+ * For more information on the delete action, see the low-level operation description in for instance * {@link DynamoDbTable#deleteItem(DeleteItemEnhancedRequest)}. * - * @param mappedTableResource the table where the key is located - * @param keyItem an item that will have its key fields used to match a record to retrieve from the database - * @param the type of modelled objects in the table + * @param mappedTableResource the table where the item is located + * @param keyItem the modeled item to be deleted as part of the transaction + * @param the type of modeled objects in the table * @return a builder of this type */ public Builder addDeleteItem(MappedTableResource mappedTableResource, T keyItem) { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java new file mode 100644 index 000000000000..3b91d83ba160 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java @@ -0,0 +1,398 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; + +import java.util.concurrent.CompletionException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.VersionedRecord; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.Record; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactDeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; + +public class OptimisticLockingAsyncCrudTest extends LocalDynamoDbAsyncTestBase { + + private static final TableSchema TABLE_SCHEMA = + StaticTableSchema.builder(Record.class) + .newItemSupplier(Record::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(Record::getId) + .setter(Record::setId) + .tags(primaryPartitionKey(), secondaryPartitionKey("index1"))) + .addAttribute(Integer.class, + a -> a.name("sort") + .getter(Record::getSort) + .setter(Record::setSort) + .tags(primarySortKey(), secondarySortKey("index1"))) + .addAttribute(Integer.class, + a -> a.name("value") + .getter(Record::getValue) + .setter(Record::setValue)) + .addAttribute(String.class, + a -> a.name("gsi_id") + .getter(Record::getGsiId) + .setter(Record::setGsiId) + .tags(secondaryPartitionKey("gsi_keys_only"))) + .addAttribute(Integer.class, + a -> a.name("gsi_sort") + .getter(Record::getGsiSort) + .setter(Record::setGsiSort) + .tags(secondarySortKey("gsi_keys_only"))) + .addAttribute(String.class, + a -> a.name("stringAttribute") + .getter(Record::getStringAttribute) + .setter(Record::setStringAttribute)) + .build(); + + private static final TableSchema VERSIONED_RECORD_TABLE_SCHEMA = + StaticTableSchema.builder(VersionedRecord.class) + .newItemSupplier(VersionedRecord::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(VersionedRecord::getId) + .setter(VersionedRecord::setId) + .tags(primaryPartitionKey(), secondaryPartitionKey("index1"))) + .addAttribute(Integer.class, + a -> a.name("sort") + .getter(VersionedRecord::getSort) + .setter(VersionedRecord::setSort) + .tags(primarySortKey(), secondarySortKey("index1"))) + .addAttribute(Integer.class, + a -> a.name("value") + .getter(VersionedRecord::getValue) + .setter(VersionedRecord::setValue)) + .addAttribute(String.class, + a -> a.name("gsi_id") + .getter(VersionedRecord::getGsiId) + .setter(VersionedRecord::setGsiId) + .tags(secondaryPartitionKey("gsi_keys_only"))) + .addAttribute(Integer.class, + a -> a.name("gsi_sort") + .getter(VersionedRecord::getGsiSort) + .setter(VersionedRecord::setGsiSort) + .tags(secondarySortKey("gsi_keys_only"))) + .addAttribute(String.class, + a -> a.name("stringAttribute") + .getter(VersionedRecord::getStringAttribute) + .setter(VersionedRecord::setStringAttribute)) + .addAttribute(Integer.class, + a -> a.name("version") + .getter(VersionedRecord::getVersion) + .setter(VersionedRecord::setVersion) + .tags(versionAttribute())) + .build(); + + + private final DynamoDbEnhancedAsyncClient enhancedClient = + DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .build(); + + private final DynamoDbAsyncTable mappedTable = + enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + private final DynamoDbAsyncTable versionedRecordTable = + enhancedClient.table(getConcreteTableName("versioned-table-name"), VERSIONED_RECORD_TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + versionedRecordTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable( + DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name")) + .build()).join(); + + getDynamoDbAsyncClient().deleteTable( + DeleteTableRequest.builder() + .tableName(getConcreteTableName("versioned-table-name")) + .build()).join(); + } + + // 1. deleteItem(T item) on Non-versioned record + // -> Optimistic Locking NOT applied -> deletes the record + @Test + public void deleteItem_onNonVersionedRecord_doesNotApplyOptimisticLockingAndDeletesTheRecord() { + Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + mappedTable.putItem(item).join(); + mappedTable.deleteItem(item).join(); + + Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 2. deleteItem(T item) on Versioned record and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItem_onVersionedRecord_whenVersionsMatch_appliesOptimisticLockingAndDeletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + versionedRecordTable.deleteItem(savedItem).join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 3. deleteItem(T item, false) on Versioned record + // -> Optimistic Locking false -> Optimistic Locking is NOT applied -> deletes the record + @Test + public void deleteItem_onVersionedRecord_noOptimisticLocking_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + // Update the item to change its version (new version = 2) + savedItem.setStringAttribute("Updated Item"); + versionedRecordTable.updateItem(savedItem).join(); + + // Delete with old version (version = 1) but flag = false - should succeed (no optimistic locking) + VersionedRecord oldVersionItem = new VersionedRecord().setId("123").setSort(10).setVersion(1); + versionedRecordTable.deleteItem(oldVersionItem, false).join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 4. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMatch_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + versionedRecordTable.deleteItem(savedItem, true).join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 5. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions DO NOT match + // -> Optimistic Locking applied -> does NOT delete the record + @Test + public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + // Update the item to change its version (new version = 2) + savedItem.setStringAttribute("Updated Item"); + versionedRecordTable.updateItem(savedItem).join(); + + // Try to delete with old version (version = 1) and flag=true - should fail + VersionedRecord oldVersionItem = new VersionedRecord().setId("123").setSort(10).setVersion(1); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(oldVersionItem, true).join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getCause()).isInstanceOf(ConditionalCheckFailedException.class)) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 6. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .withOptimisticLocking(matchVersion, "version") + .build(); + + versionedRecordTable.deleteItem(requestWithLocking).join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // -> Optimistic Locking is applied -> does NOT delete the record + @Test + public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .withOptimisticLocking(mismatchVersion, "version") + .build(); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking).join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + + // 8. TransactWriteItems.addDeleteItem(T item) on Non-versioned record + // -> Optimistic Locking NOT applied -> deletes the record + @Test + public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { + Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + mappedTable.putItem(item).join(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(mappedTable, item) + .build()).join(); + + Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 9. TransactWriteItems.addDeleteItem(T item) on Versioned record and versions match + // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record + @Test + public void transactDeleteItem_versionedRecord_versionsMatch_shouldSucceed() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, savedItem) + .build()) + .join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 10. TransactWriteItems.addDeleteItem(T item) on Versioned record and versions do NOT match + // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record + @Test + public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_doesNotApplyOptimisticLockingAndDeletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + Integer mismatchedVersion = 2; + savedItem.setVersion(mismatchedVersion); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, savedItem) + .build()) + .join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 11. TransactWriteItems with builder method on Versioned record and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .withOptimisticLocking(matchVersion, "version") + .build(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build()) + .join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 12. TransactWriteItems with builder method on Versioned record and versions do NOT match + // -> Optimistic Locking applied -> does NOT delete the record + @Test + public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .withOptimisticLocking(mismatchVersion, "version") + .build(); + + assertThatThrownBy(() -> enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, + requestWithLocking) + .build()) + .join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(((TransactionCanceledException) e.getCause()) + .cancellationReasons() + .stream() + .anyMatch(reason -> "ConditionalCheckFailed".equals(reason.code()))) + .isTrue()); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java new file mode 100644 index 000000000000..1f8de825caec --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java @@ -0,0 +1,394 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.VersionedRecord; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.Record; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactDeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; + +public class OptimisticLockingCrudTest extends LocalDynamoDbSyncTestBase { + + private static final TableSchema TABLE_SCHEMA = + StaticTableSchema.builder(Record.class) + .newItemSupplier(Record::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(Record::getId) + .setter(Record::setId) + .tags(primaryPartitionKey(), secondaryPartitionKey("index1"))) + .addAttribute(Integer.class, + a -> a.name("sort") + .getter(Record::getSort) + .setter(Record::setSort) + .tags(primarySortKey(), secondarySortKey("index1"))) + .addAttribute(Integer.class, + a -> a.name("value") + .getter(Record::getValue) + .setter(Record::setValue)) + .addAttribute(String.class, + a -> a.name("gsi_id") + .getter(Record::getGsiId) + .setter(Record::setGsiId) + .tags(secondaryPartitionKey("gsi_keys_only"))) + .addAttribute(Integer.class, + a -> a.name("gsi_sort") + .getter(Record::getGsiSort) + .setter(Record::setGsiSort) + .tags(secondarySortKey("gsi_keys_only"))) + .addAttribute(String.class, + a -> a.name("stringAttribute") + .getter(Record::getStringAttribute) + .setter(Record::setStringAttribute)) + .build(); + + private static final TableSchema VERSIONED_RECORD_TABLE_SCHEMA = + StaticTableSchema.builder(VersionedRecord.class) + .newItemSupplier(VersionedRecord::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(VersionedRecord::getId) + .setter(VersionedRecord::setId) + .tags(primaryPartitionKey(), secondaryPartitionKey("index1"))) + .addAttribute(Integer.class, + a -> a.name("sort") + .getter(VersionedRecord::getSort) + .setter(VersionedRecord::setSort) + .tags(primarySortKey(), secondarySortKey("index1"))) + .addAttribute(Integer.class, + a -> a.name("value") + .getter(VersionedRecord::getValue) + .setter(VersionedRecord::setValue)) + .addAttribute(String.class, + a -> a.name("gsi_id") + .getter(VersionedRecord::getGsiId) + .setter(VersionedRecord::setGsiId) + .tags(secondaryPartitionKey("gsi_keys_only"))) + .addAttribute(Integer.class, + a -> a.name("gsi_sort") + .getter(VersionedRecord::getGsiSort) + .setter(VersionedRecord::setGsiSort) + .tags(secondarySortKey("gsi_keys_only"))) + .addAttribute(String.class, + a -> a.name("stringAttribute") + .getter(VersionedRecord::getStringAttribute) + .setter(VersionedRecord::setStringAttribute)) + .addAttribute(Integer.class, + a -> a.name("version") + .getter(VersionedRecord::getVersion) + .setter(VersionedRecord::setVersion) + .tags(versionAttribute())) + .build(); + + + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .build(); + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + private final DynamoDbTable versionedRecordTable = + enhancedClient.table(getConcreteTableName("versioned-table-name"), VERSIONED_RECORD_TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + versionedRecordTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable( + DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name")) + .build()); + + getDynamoDbClient().deleteTable( + DeleteTableRequest.builder() + .tableName(getConcreteTableName("versioned-table-name")) + .build()); + } + + // 1. deleteItem(T item) on Non-versioned record + // -> Optimistic Locking NOT applied -> deletes the record + @Test + public void deleteItem_onNonVersionedRecord_doesNotApplyOptimisticLockingAndDeletesTheRecord() { + Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + mappedTable.putItem(item); + mappedTable.deleteItem(item); + + Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 2. deleteItem(T item) on Versioned record and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItem_onVersionedRecord_whenVersionsMatch_appliesOptimisticLockingAndDeletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + versionedRecordTable.deleteItem(savedItem); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 3. deleteItem(T item, false) on Versioned record + // -> Optimistic Locking false -> Optimistic Locking is NOT applied -> deletes the record + @Test + public void deleteItem_onVersionedRecord_noOptimisticLocking_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + // Update the item to change its version (new version = 2) + savedItem.setStringAttribute("Updated Item"); + versionedRecordTable.updateItem(savedItem); + + // Delete with old version (version = 1) but flag = false - should succeed (no optimistic locking) + VersionedRecord oldVersionItem = new VersionedRecord().setId("123").setSort(10).setVersion(1); + versionedRecordTable.deleteItem(oldVersionItem, false); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 4. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMatch_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + versionedRecordTable.deleteItem(savedItem, true); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 5. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions DO NOT match + // -> Optimistic Locking applied -> does NOT delete the record + @Test + public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + // Update the item to change its version (new version = 2) + savedItem.setStringAttribute("Updated Item"); + versionedRecordTable.updateItem(savedItem); + + // Try to delete with old version (version = 1) and flag = true - should fail + VersionedRecord oldVersionItem = new VersionedRecord().setId("123").setSort(10).setVersion(1); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(oldVersionItem, true)) + .isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 6. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .withOptimisticLocking(matchVersion, "version") + .build(); + + versionedRecordTable.deleteItem(requestWithLocking); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // -> Optimistic Locking is applied -> does NOT delete the record + @Test + public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .withOptimisticLocking(mismatchVersion, "version") + .build(); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking)) + .isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + + // 8. TransactWriteItems.addDeleteItem(T item) on Non-versioned record + // -> Optimistic Locking NOT applied -> deletes the record + @Test + public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { + Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + mappedTable.putItem(item); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(mappedTable, item) + .build()); + + Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 9. TransactWriteItems.addDeleteItem(T item) on Versioned record and versions match + // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record + @Test + public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_doesNotApplyOptimisticLockingAndDeletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, savedItem) + .build()); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 10. TransactWriteItems.addDeleteItem(T item) on Versioned record and versions do NOT match + // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record + @Test + public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_doesNotApplyOptimisticLockingAndDeletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + Integer mismatchedVersion = 2; + savedItem.setVersion(mismatchedVersion); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, savedItem) + .build()); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 11. TransactWriteItems with builder method on Versioned record and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .withOptimisticLocking(matchVersion, "version") + .build(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build()); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 12. TransactWriteItems with builder method on Versioned record and versions do NOT match + // -> Optimistic Locking applied -> does NOT delete the record + @Test + public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .withOptimisticLocking(mismatchVersion, "version") + .build(); + + TransactionCanceledException ex = + assertThrows(TransactionCanceledException.class, + () -> enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build())); + + assertTrue(ex.hasCancellationReasons()); + assertEquals(1, ex.cancellationReasons().size()); + assertEquals("ConditionalCheckFailed", ex.cancellationReasons().get(0).code()); + assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/VersionedRecord.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/VersionedRecord.java new file mode 100644 index 000000000000..a1bec12d70e4 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/VersionedRecord.java @@ -0,0 +1,122 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class VersionedRecord { + + private String id; + private Integer sort; + private Integer value; + private String gsiId; + private Integer gsiSort; + + private String stringAttribute; + private Integer version; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public VersionedRecord setId(String id) { + this.id = id; + return this; + } + + public Integer getSort() { + return sort; + } + + public VersionedRecord setSort(Integer sort) { + this.sort = sort; + return this; + } + + public Integer getValue() { + return value; + } + + public VersionedRecord setValue(Integer value) { + this.value = value; + return this; + } + + public String getGsiId() { + return gsiId; + } + + public VersionedRecord setGsiId(String gsiId) { + this.gsiId = gsiId; + return this; + } + + public Integer getGsiSort() { + return gsiSort; + } + + public VersionedRecord setGsiSort(Integer gsiSort) { + this.gsiSort = gsiSort; + return this; + } + + public String getStringAttribute() { + return stringAttribute; + } + + public VersionedRecord setStringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + return this; + } + + @DynamoDbVersionAttribute + public Integer getVersion() { + return version; + } + + public VersionedRecord setVersion(Integer version) { + this.version = version; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VersionedRecord versionedRecord = (VersionedRecord) o; + return Objects.equals(id, versionedRecord.id) && + Objects.equals(sort, versionedRecord.sort) && + Objects.equals(value, versionedRecord.value) && + Objects.equals(gsiId, versionedRecord.gsiId) && + Objects.equals(stringAttribute, versionedRecord.stringAttribute) && + Objects.equals(gsiSort, versionedRecord.gsiSort) && + Objects.equals(version, versionedRecord.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, value, gsiId, gsiSort, stringAttribute, version); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java index 1a75d402e275..ef81bf79e4fb 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.model; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -27,6 +28,7 @@ import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; import software.amazon.awssdk.services.dynamodb.model.ReturnItemCollectionMetrics; import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; @@ -269,4 +271,19 @@ public void hashCode_returnValuesOnConditionCheckFailure() { assertThat(containsKey.hashCode(), not(equalTo(emptyRequest.hashCode()))); } + + @Test + public void withOptimisticLockingBuilder_addsVersinConditionExpression() { + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + + DeleteItemEnhancedRequest request = + DeleteItemEnhancedRequest.builder() + .key(Key.builder().partitionValue("id").build()) + .withOptimisticLocking(versionValue, "version") + .build(); + + assertThat(request.conditionExpression(), notNullValue()); + assertThat(request.conditionExpression().expression(), is("version = :version_value")); + assertThat(request.conditionExpression().expressionValues().get(":version_value"), equalTo(versionValue)); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java new file mode 100644 index 000000000000..cf1a8edeb4e0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java @@ -0,0 +1,408 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Optional; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordForUpdateExpressions; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class OptimisticLockingHelperTest { + + @Test + public void withOptimisticLocking_onDelete_addsConditionExpression() { + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + DeleteItemEnhancedRequest originalRequest = + DeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + DeleteItemEnhancedRequest result = + OptimisticLockingHelper.withOptimisticLocking(originalRequest, versionValue, versionAttributeName); + + assertThat(result).isNotNull(); + assertThat(result.conditionExpression()).isNotNull(); + assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } + + @Test + public void withOptimisticLocking_onTransactDelete_addsConditionExpression() { + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + TransactDeleteItemEnhancedRequest result = + OptimisticLockingHelper.withOptimisticLocking(originalRequest, versionValue, versionAttributeName); + + assertThat(result).isNotNull(); + assertThat(result.conditionExpression()).isNotNull(); + assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } + + @Test + public void conditionallyApplyOptimistic_onDelete_whenFlagFalse_returnsOriginalRequest() { + boolean optimisticLockingEnabled = false; + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + // versioned record + RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); + TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); + + DeleteItemEnhancedRequest originalRequest = + DeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + DeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + + assertThat(result).isNotNull(); + assertEquals(originalRequest, result); + } + + @Test + public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecord_returnsOriginalRequest() { + boolean optimisticLockingEnabled = true; + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + // non-versioned record + RecordForUpdateExpressions keyItem = new RecordForUpdateExpressions(); + TableSchema tableSchema = TableSchema.fromClass(RecordForUpdateExpressions.class); + + DeleteItemEnhancedRequest originalRequest = + DeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + DeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + + assertThat(result).isNotNull(); + assertThat(result.conditionExpression()).isNotNull(); + assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } + + @Test + public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecordWithoutVersion_returnsOriginalRequest() { + boolean optimisticLockingEnabled = true; + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = null; + String versionAttributeName = "version"; + + // versioned record + RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); + TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); + + DeleteItemEnhancedRequest originalRequest = + DeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + DeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + + assertThat(result).isNotNull(); + assertEquals(originalRequest, result); + } + + @Test + public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecordWithVersion_addsConditionExpression() { + boolean optimisticLockingEnabled = true; + Key key = Key.builder().partitionValue("id").build(); + Long version = 1L; + AttributeValue versionValue = AttributeValue.builder().n(String.valueOf(version)).build(); + String versionAttributeName = "version"; + + // versioned record + RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); + keyItem.setVersion(version); + TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); + + DeleteItemEnhancedRequest originalRequest = + DeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + DeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + + assertThat(result).isNotNull(); + assertThat(result.conditionExpression()).isNotNull(); + assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } + + @Test + public void conditionallyApplyOptimistic_onTransactDelete_whenFlagFalse_returnsOriginalRequest() { + boolean optimisticLockingEnabled = false; + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + // versioned record + RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); + TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); + + TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + TransactDeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + + assertThat(result).isNotNull(); + assertEquals(originalRequest, result); + } + + @Test + public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndNonVersionedRecord_returnsOriginalRequest() { + boolean optimisticLockingEnabled = true; + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + // non-versioned record + RecordForUpdateExpressions keyItem = new RecordForUpdateExpressions(); + TableSchema tableSchema = TableSchema.fromClass(RecordForUpdateExpressions.class); + + TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + TransactDeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + + assertThat(result).isNotNull(); + assertThat(result.conditionExpression()).isNotNull(); + assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } + + @Test + public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersionedRecord_addsConditionExpression() { + boolean optimisticLockingEnabled = true; + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + // versioned record + RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); + TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); + + TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + TransactDeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + + assertThat(result).isNotNull(); + assertEquals(originalRequest, result); + } + + @Test + public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersionedRecordWithVersion_addsConditionExpression() { + boolean optimisticLockingEnabled = true; + Key key = Key.builder().partitionValue("id").build(); + Long version = 1L; + AttributeValue versionValue = AttributeValue.builder().n(String.valueOf(version)).build(); + String versionAttributeName = "version"; + + // versioned record + RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); + keyItem.setVersion(version); + TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); + + TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + TransactDeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + + assertThat(result).isNotNull(); + assertThat(result.conditionExpression()).isNotNull(); + assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } + + @Test + public void createVersionCondition_shouldCreateCorrectExpression() { + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + Expression result = OptimisticLockingHelper.createVersionCondition(versionValue, versionAttributeName); + + assertThat(result.expression()).isEqualTo("version = :version_value"); + assertThat(result.expressionValues()).containsEntry(":version_value", versionValue); + } + + @Test + public void getVersionAttributeName_forVersionedRecord_returnsTheCorrectVersionValueFromTheTableSchema() { + // versioned record + TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); + Optional versionAttributeNameOpt = OptimisticLockingHelper.getVersionAttributeName(tableSchema); + + assertNotNull(versionAttributeNameOpt); + assertTrue(versionAttributeNameOpt.isPresent()); + assertThat(versionAttributeNameOpt.get()).isEqualTo("version"); + } + + @Test + public void getVersionAttributeName_forNonVersionedRecord_shouldNotReturnAVersionValue() { + // non-versioned record + TableSchema tableSchema = TableSchema.fromClass(RecordForUpdateExpressions.class); + Optional versionAttributeNameOpt = OptimisticLockingHelper.getVersionAttributeName(tableSchema); + + assertNotNull(versionAttributeNameOpt); + assertFalse(versionAttributeNameOpt.isPresent()); + } + + @Test + public void buildDeleteItemEnhancedRequest_addsCorrectExpressionOnRequest() { + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + DeleteItemEnhancedRequest result = + DeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + assertThat(result.key()).isEqualTo(key); + assertThat(result.conditionExpression()).isNotNull(); + assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } + + @Test + public void buildDeleteItemEnhancedRequest_differentVersionAttributeNames_addsCorrectExpressionOnRequest() { + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + + // Test with different attribute names + String[] attributeNames = {"version", "recordVersion", "itemVersion", "v"}; + + for (String attributeName : attributeNames) { + DeleteItemEnhancedRequest result = + DeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, attributeName) + .build(); + + assertThat(result.conditionExpression().expression()).isEqualTo(attributeName + " = :version_value"); + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } + } + + @Test + public void buildDeleteItemEnhancedRequest_differentVersionValues_addsCorrectExpressionOnRequest() { + Key key = Key.builder().partitionValue("test-id").build(); + + // Test with different version values + AttributeValue[] versionValues = { + AttributeValue.builder().n("0").build(), + AttributeValue.builder().n("1").build(), + AttributeValue.builder().n("999").build(), + AttributeValue.builder().n("123456789").build() + }; + + for (AttributeValue versionValue : versionValues) { + DeleteItemEnhancedRequest result = + DeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, "version") + .build(); + + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } + } + + @Test + public void buildDeleteItemEnhancedRequest_preservesExistingRequestProperties() { + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + + DeleteItemEnhancedRequest result = + DeleteItemEnhancedRequest.builder() + .key(key) + .returnConsumedCapacity("TOTAL") + .withOptimisticLocking(versionValue, "version") + .build(); + + assertThat(result.key()).isEqualTo(key); + assertThat(result.returnConsumedCapacityAsString()).isEqualTo("TOTAL"); + assertThat(result.conditionExpression()).isNotNull(); + } + + @Test + public void buildTransactDeleteItemEnhancedRequest_addsCorrectExpressionOnRequest() { + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "recordVersion"; + + TransactDeleteItemEnhancedRequest result = + TransactDeleteItemEnhancedRequest.builder() + .key(key) + .withOptimisticLocking(versionValue, versionAttributeName) + .build(); + + assertThat(result.key()).isEqualTo(key); + assertThat(result.conditionExpression()).isNotNull(); + assertThat(result.conditionExpression().expression()).isEqualTo("recordVersion = :version_value"); + assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequestTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequestTest.java index dc33f4c07696..5ccb0c3b87f8 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequestTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequestTest.java @@ -111,6 +111,16 @@ public void equals_self() { assertThat(builtObject, equalTo(builtObject)); } + @Test + public void equals_NullObject() { + Key key1 = Key.builder().partitionValue("key1").build(); + + TransactDeleteItemEnhancedRequest builtObject1 = TransactDeleteItemEnhancedRequest.builder().key(key1).build(); + TransactDeleteItemEnhancedRequest builtObject2 = null; + + assertThat(builtObject1, not(equalTo(builtObject2))); + } + @Test public void equals_keyNotEqual() { Key key1 = Key.builder().partitionValue("key1").build(); From 5adbe18ec0b441d6da030e23f0442806c4a730f0 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 25 Feb 2026 10:05:17 +0200 Subject: [PATCH 02/14] Address PR feedback --- .../enhanced/dynamodb/DynamoDbTable.java | 7 ++++++ .../model/OptimisticLockingHelper.java | 8 ++++++- .../model/OptimisticLockingHelperTest.java | 23 ++++++++++--------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java index 49f19a8842dd..4f42298dc177 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java @@ -245,6 +245,13 @@ default T deleteItem(T keyItem) { throw new UnsupportedOperationException(); } + /** + * Deletes an item from the table with optional optimistic locking. + * + * @param keyItem the item containing the key to delete + * @param useOptimisticLocking if true, applies optimistic locking if the item has version information + * @return the deleted item, or null if the item was not found + */ default T deleteItem(T keyItem, boolean useOptimisticLocking) { throw new UnsupportedOperationException(); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java index 113af4709ecb..2fc27a86d9c6 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java @@ -15,6 +15,9 @@ package software.amazon.awssdk.enhanced.dynamodb.model; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef; + +import java.util.Collections; import java.util.Optional; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.Expression; @@ -126,9 +129,12 @@ public static TransactDeleteItemEnhancedRequest conditionallyApplyOptimistic * @return version check condition expression */ public static Expression createVersionCondition(AttributeValue versionValue, String versionAttributeName) { + String attributeKeyRef = keyRef(versionAttributeName); + return Expression.builder() - .expression(versionAttributeName + " = :version_value") + .expression(String.format("%s = :version_value", attributeKeyRef)) .putExpressionValue(":version_value", versionValue) + .expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeName)) .build(); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java index cf1a8edeb4e0..8f74c6368e07 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java @@ -49,7 +49,7 @@ public void withOptimisticLocking_onDelete_addsConditionExpression() { assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } @@ -70,7 +70,7 @@ public void withOptimisticLocking_onTransactDelete_addsConditionExpression() { assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } @@ -120,7 +120,7 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecord assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } @@ -172,7 +172,7 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecord assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } @@ -222,12 +222,12 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndNonVers assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } @Test - public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersionedRecord_addsConditionExpression() { + public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersionedRecordWithNullVersion_returnsOriginalRequest() { boolean optimisticLockingEnabled = true; Key key = Key.builder().partitionValue("id").build(); AttributeValue versionValue = AttributeValue.builder().n("1").build(); @@ -274,7 +274,7 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersion assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } @@ -285,7 +285,7 @@ public void createVersionCondition_shouldCreateCorrectExpression() { Expression result = OptimisticLockingHelper.createVersionCondition(versionValue, versionAttributeName); - assertThat(result.expression()).isEqualTo("version = :version_value"); + assertThat(result.expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); assertThat(result.expressionValues()).containsEntry(":version_value", versionValue); } @@ -324,7 +324,7 @@ public void buildDeleteItemEnhancedRequest_addsCorrectExpressionOnRequest() { assertThat(result.key()).isEqualTo(key); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("version = :version_value"); + assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } @@ -343,7 +343,8 @@ public void buildDeleteItemEnhancedRequest_differentVersionAttributeNames_addsCo .withOptimisticLocking(versionValue, attributeName) .build(); - assertThat(result.conditionExpression().expression()).isEqualTo(attributeName + " = :version_value"); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_" + attributeName + " = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } } @@ -402,7 +403,7 @@ public void buildTransactDeleteItemEnhancedRequest_addsCorrectExpressionOnReques assertThat(result.key()).isEqualTo(key); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("recordVersion = :version_value"); + assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_recordVersion = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } } \ No newline at end of file From cc414f35465c0a4d3edea2368f6281b4f4310302 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 25 Feb 2026 10:56:08 +0200 Subject: [PATCH 03/14] Address PR feedback --- .../dynamodb/internal/client/DefaultDynamoDbTable.java | 2 +- .../dynamodb/model/DeleteItemEnhancedRequestTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java index bf219097a444..6277c4cda605 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java @@ -160,7 +160,7 @@ public T deleteItem(Key key) { @Override @Deprecated public T deleteItem(T keyItem) { - return deleteItem(keyItem, false); + return deleteItem(keyFrom(keyItem)); } /** diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java index ef81bf79e4fb..f9baa2bc83bf 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java @@ -273,7 +273,7 @@ public void hashCode_returnValuesOnConditionCheckFailure() { } @Test - public void withOptimisticLockingBuilder_addsVersinConditionExpression() { + public void withOptimisticLockingBuilder_addsVersionConditionExpression() { AttributeValue versionValue = AttributeValue.builder().n("1").build(); DeleteItemEnhancedRequest request = @@ -283,7 +283,7 @@ public void withOptimisticLockingBuilder_addsVersinConditionExpression() { .build(); assertThat(request.conditionExpression(), notNullValue()); - assertThat(request.conditionExpression().expression(), is("version = :version_value")); + assertThat(request.conditionExpression().expression(), is("#AMZN_MAPPED_version = :version_value")); assertThat(request.conditionExpression().expressionValues().get(":version_value"), equalTo(versionValue)); } } From 3a84c27f03c145a819d79fca71611a40ab9b8829 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 25 Feb 2026 11:17:17 +0200 Subject: [PATCH 04/14] Address PR feedback --- .../awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java | 8 ++++++++ .../amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java | 1 + 2 files changed, 9 insertions(+) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java index 135302d54b05..24729658c9f3 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java @@ -243,10 +243,18 @@ default CompletableFuture deleteItem(Key key) { * delete from the database table. * @return a {@link CompletableFuture} of the item that was persisted in the database before it was deleted. */ + @Deprecated default CompletableFuture deleteItem(T keyItem) { throw new UnsupportedOperationException(); } + /** + * Deletes an item from the table with optional optimistic locking. + * + * @param keyItem the item containing the key to delete + * @param useOptimisticLocking if true, applies optimistic locking if the item has version information + * @return a CompletableFuture containing the deleted item, or null if the item was not found + */ default CompletableFuture deleteItem(T keyItem, boolean useOptimisticLocking) { throw new UnsupportedOperationException(); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java index 4f42298dc177..8daf5c49eca3 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java @@ -241,6 +241,7 @@ default T deleteItem(Key key) { * delete from the database table. * @return The item that was persisted in the database before it was deleted. */ + @Deprecated default T deleteItem(T keyItem) { throw new UnsupportedOperationException(); } From fd025d17fbf59ec9086bc3648f665b90d85d31ef Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 25 Feb 2026 11:46:12 +0200 Subject: [PATCH 05/14] Address PR feedback --- .../model/DeleteItemEnhancedRequest.java | 1 - .../model/OptimisticLockingHelper.java | 5 +++++ .../TransactDeleteItemEnhancedRequest.java | 1 - .../model/OptimisticLockingHelperTest.java | 22 ++++++++++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java index a27379596331..00018f515f99 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java @@ -301,7 +301,6 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio * @param versionValue the expected version value that must match for the deletion to succeed * @param versionAttributeName the name of the version attribute in the DynamoDB table * @return a builder of this type with optimistic locking condition applied - * @throws IllegalArgumentException if any parameter is null */ public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) { Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java index 2fc27a86d9c6..ff49df7c1295 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java @@ -127,8 +127,13 @@ public static TransactDeleteItemEnhancedRequest conditionallyApplyOptimistic * @param versionValue the expected version value * @param versionAttributeName the version attribute name * @return version check condition expression + * @throws IllegalArgumentException if {@code versionAttributeName} is null or empty */ public static Expression createVersionCondition(AttributeValue versionValue, String versionAttributeName) { + if (versionAttributeName == null || versionAttributeName.trim().isEmpty()) { + throw new IllegalArgumentException("Version attribute name must not be null or empty."); + } + String attributeKeyRef = keyRef(versionAttributeName); return Expression.builder() diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java index fdbad8baa983..9293103aef84 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java @@ -227,7 +227,6 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio * @param versionValue the expected version value that must match for the deletion to succeed * @param versionAttributeName the name of the version attribute in the DynamoDB table * @return a builder of this type with optimistic locking condition applied - * @throws IllegalArgumentException if any parameter is null */ public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) { Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java index 8f74c6368e07..5d0eb1e64f8f 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.model; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -289,6 +290,24 @@ public void createVersionCondition_shouldCreateCorrectExpression() { assertThat(result.expressionValues()).containsEntry(":version_value", versionValue); } + @Test + public void createVersionCondition_nullAttributeName_throwsIllegalArgumentException_withMessage() { + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + + assertThatThrownBy(() -> OptimisticLockingHelper.createVersionCondition(versionValue, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Version attribute name must not be null or empty."); + } + + @Test + public void createVersionCondition_emptyAttributeName_throwsIllegalArgumentException_withMessage() { + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + + assertThatThrownBy(() -> OptimisticLockingHelper.createVersionCondition(versionValue, "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Version attribute name must not be null or empty."); + } + @Test public void getVersionAttributeName_forVersionedRecord_returnsTheCorrectVersionValueFromTheTableSchema() { // versioned record @@ -406,4 +425,5 @@ public void buildTransactDeleteItemEnhancedRequest_addsCorrectExpressionOnReques assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_recordVersion = :version_value"); assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); } -} \ No newline at end of file +} + From b151dfdd8a7c02216d61bc7c69be675dd6abb01f Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 25 Feb 2026 12:18:34 +0200 Subject: [PATCH 06/14] Address PR feedback --- .../dynamodb/internal/client/DefaultDynamoDbAsyncTable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java index a42d2c5ba13b..09f927b88211 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java @@ -159,7 +159,7 @@ public CompletableFuture deleteItem(Key key) { @Override @Deprecated public CompletableFuture deleteItem(T keyItem) { - return deleteItem(keyItem, false); + return deleteItem(keyFrom(keyItem)); } /** From e49d02afe1af7341d81d76df14a7cb211e9a71a7 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 25 Feb 2026 12:49:35 +0200 Subject: [PATCH 07/14] Address PR feedback --- .../OptimisticLockingHelper.java | 8 +++++--- .../internal/client/DefaultDynamoDbAsyncTable.java | 7 ++++--- .../internal/client/DefaultDynamoDbTable.java | 7 ++++--- .../dynamodb/model/DeleteItemEnhancedRequest.java | 14 ++++++++------ .../model/TransactDeleteItemEnhancedRequest.java | 14 ++++++++------ .../model/OptimisticLockingHelperTest.java | 1 + 6 files changed, 30 insertions(+), 21 deletions(-) rename services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/{model => internal}/OptimisticLockingHelper.java (95%) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java similarity index 95% rename from services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java rename to services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java index ff49df7c1295..cffd50b28bfe 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelper.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java @@ -13,15 +13,17 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.enhanced.dynamodb.model; +package software.amazon.awssdk.enhanced.dynamodb.internal; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef; import java.util.Collections; import java.util.Optional; -import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactDeleteItemEnhancedRequest; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; /** @@ -30,7 +32,7 @@ * Optimistic locking prevents concurrent modifications by checking that an item's version hasn't changed since it was last read. * If the version has changed, the delete operation fails with a {@code ConditionalCheckFailedException}. */ -@SdkPublicApi +@SdkInternalApi public final class OptimisticLockingHelper { private OptimisticLockingHelper() { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java index 09f927b88211..a6e10de68405 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java @@ -16,7 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.client; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.createKeyFromItem; -import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.conditionallyApplyOptimisticLocking; +import static software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper.conditionallyApplyOptimisticLocking; import java.util.ArrayList; import java.util.concurrent.CompletableFuture; @@ -27,6 +27,7 @@ import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper; import software.amazon.awssdk.enhanced.dynamodb.internal.TableIndices; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.CreateTableOperation; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteItemOperation; @@ -126,7 +127,7 @@ public CompletableFuture createTable() { } /** - * Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}. + * Supports optimistic locking via {@link OptimisticLockingHelper}. */ @Override public CompletableFuture deleteItem(DeleteItemEnhancedRequest request) { @@ -136,7 +137,7 @@ public CompletableFuture deleteItem(DeleteItemEnhancedRequest request) { } /** - * Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}. + * Supports optimistic locking via {@link OptimisticLockingHelper}. */ @Override public CompletableFuture deleteItem(Consumer requestConsumer) { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java index 6277c4cda605..f4742e79f097 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java @@ -16,7 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.client; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.createKeyFromItem; -import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.conditionallyApplyOptimisticLocking; +import static software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper.conditionallyApplyOptimisticLocking; import java.util.ArrayList; import java.util.function.Consumer; @@ -26,6 +26,7 @@ import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper; import software.amazon.awssdk.enhanced.dynamodb.internal.TableIndices; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.CreateTableOperation; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteItemOperation; @@ -128,7 +129,7 @@ public void createTable() { } /** - * Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}. + * Supports optimistic locking via {@link OptimisticLockingHelper}. */ @Override public T deleteItem(DeleteItemEnhancedRequest request) { @@ -137,7 +138,7 @@ public T deleteItem(DeleteItemEnhancedRequest request) { } /** - * Supports optimistic locking via {@link software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper}. + * Supports optimistic locking via {@link OptimisticLockingHelper}. */ @Override public T deleteItem(Consumer requestConsumer) { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java index 00018f515f99..689674ec1b50 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java @@ -15,7 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.model; -import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.createVersionCondition; +import static software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper.createVersionCondition; import java.util.Objects; import java.util.function.Consumer; @@ -295,16 +295,18 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio /** * Adds optimistic locking to this delete request. *

- * This method applies a condition expression that ensures the delete operation only succeeds - * if the version attribute of the item matches the provided expected value. + * If a {@link #conditionExpression(Expression)} was already set, this will combine it with the optimistic locking + * condition using {@code AND}. If either expression has conflicting name/value tokens, {@link Expression#join} will throw + * {@link IllegalArgumentException}. * - * @param versionValue the expected version value that must match for the deletion to succeed + * @param versionValue the expected version value that must match for the deletion to succeed * @param versionAttributeName the name of the version attribute in the DynamoDB table - * @return a builder of this type with optimistic locking condition applied + * @return a builder of this type with optimistic locking condition applied (and merged if needed) */ public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) { Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName); - return conditionExpression(optimisticLockingCondition); + this.conditionExpression = Expression.join(this.conditionExpression, optimisticLockingCondition, " AND "); + return this; } public DeleteItemEnhancedRequest build() { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java index 9293103aef84..4e757596af7b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java @@ -15,7 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.model; -import static software.amazon.awssdk.enhanced.dynamodb.model.OptimisticLockingHelper.createVersionCondition; +import static software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper.createVersionCondition; import java.util.Objects; import java.util.function.Consumer; @@ -221,16 +221,18 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio /** * Adds optimistic locking to this transactional delete request. *

- * This method applies a condition expression that ensures the delete operation only succeeds if the version attribute of - * the item matches the provided expected value. If the condition fails, the entire transaction will be cancelled. + * If a {@link #conditionExpression(Expression)} was already set, this will combine it with the optimistic locking + * condition using {@code AND}. If either expression has conflicting name/value tokens, {@link Expression#join} will throw + * {@link IllegalArgumentException}. * - * @param versionValue the expected version value that must match for the deletion to succeed + * @param versionValue the expected version value that must match for the deletion to succeed * @param versionAttributeName the name of the version attribute in the DynamoDB table - * @return a builder of this type with optimistic locking condition applied + * @return a builder of this type with optimistic locking condition applied (and merged if needed) */ public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) { Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName); - return conditionExpression(optimisticLockingCondition); + this.conditionExpression = Expression.join(this.conditionExpression, optimisticLockingCondition, " AND "); + return this; } public TransactDeleteItemEnhancedRequest build() { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java index 5d0eb1e64f8f..a593faac4ae2 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java @@ -29,6 +29,7 @@ import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordForUpdateExpressions; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors; +import software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; public class OptimisticLockingHelperTest { From e28e78ad4d3a5da659f430563cb23a78157d9119 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Thu, 26 Feb 2026 20:23:54 +0200 Subject: [PATCH 08/14] Address PR feedback --- .../internal/OptimisticLockingHelper.java | 8 +- .../model/DeleteItemEnhancedRequest.java | 2 +- .../TransactDeleteItemEnhancedRequest.java | 2 +- .../TransactWriteItemsEnhancedRequest.java | 2 +- .../OptimisticLockingAsyncCrudTest.java | 463 +++++++++++++++--- .../OptimisticLockingCrudTest.java | 419 ++++++++++++++-- .../model/DeleteItemEnhancedRequestTest.java | 4 +- .../model/OptimisticLockingHelperTest.java | 165 +++++-- 8 files changed, 917 insertions(+), 148 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java index cffd50b28bfe..92fa6d413683 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java @@ -46,7 +46,7 @@ private OptimisticLockingHelper() { * @param versionAttributeName the version attribute name * @return delete request with optimistic locking condition */ - public static DeleteItemEnhancedRequest withOptimisticLocking( + public static DeleteItemEnhancedRequest optimisticLocking( DeleteItemEnhancedRequest request, AttributeValue versionValue, String versionAttributeName) { Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName); @@ -63,7 +63,7 @@ public static DeleteItemEnhancedRequest withOptimisticLocking( * @param versionAttributeName the version attribute name * @return transactional delete request with optimistic locking condition */ - public static TransactDeleteItemEnhancedRequest withOptimisticLocking( + public static TransactDeleteItemEnhancedRequest optimisticLocking( TransactDeleteItemEnhancedRequest request, AttributeValue versionValue, String versionAttributeName) { Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName); @@ -92,7 +92,7 @@ public static DeleteItemEnhancedRequest conditionallyApplyOptimisticLocking( return getVersionAttributeName(tableSchema) .map(versionAttributeName -> { AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName); - return version != null ? withOptimisticLocking(request, version, versionAttributeName) : request; + return version != null ? optimisticLocking(request, version, versionAttributeName) : request; }) .orElse(request); } @@ -117,7 +117,7 @@ public static TransactDeleteItemEnhancedRequest conditionallyApplyOptimistic return getVersionAttributeName(tableSchema) .map(versionAttributeName -> { AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName); - return version != null ? withOptimisticLocking(request, version, versionAttributeName) : request; + return version != null ? optimisticLocking(request, version, versionAttributeName) : request; }) .orElse(request); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java index 689674ec1b50..cf5592326a28 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequest.java @@ -303,7 +303,7 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio * @param versionAttributeName the name of the version attribute in the DynamoDB table * @return a builder of this type with optimistic locking condition applied (and merged if needed) */ - public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) { + public Builder optimisticLocking(AttributeValue versionValue, String versionAttributeName) { Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName); this.conditionExpression = Expression.join(this.conditionExpression, optimisticLockingCondition, " AND "); return this; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java index 4e757596af7b..fe30e84d4629 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactDeleteItemEnhancedRequest.java @@ -229,7 +229,7 @@ public Builder returnValuesOnConditionCheckFailure(String returnValuesOnConditio * @param versionAttributeName the name of the version attribute in the DynamoDB table * @return a builder of this type with optimistic locking condition applied (and merged if needed) */ - public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) { + public Builder optimisticLocking(AttributeValue versionValue, String versionAttributeName) { Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName); this.conditionExpression = Expression.join(this.conditionExpression, optimisticLockingCondition, " AND "); return this; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactWriteItemsEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactWriteItemsEnhancedRequest.java index b6e81f0150fe..6dbbe0d01695 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactWriteItemsEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactWriteItemsEnhancedRequest.java @@ -248,7 +248,7 @@ public Builder addDeleteItem(MappedTableResource mappedTableResource, Del * {@link TransactDeleteItemEnhancedRequest}. *

* For optimistic locking support, use - * {@link TransactDeleteItemEnhancedRequest.Builder#withOptimisticLocking( + * {@link TransactDeleteItemEnhancedRequest.Builder#optimisticLocking( * software.amazon.awssdk.services.dynamodb.model.AttributeValue, String)} * to create a request with version checking conditions before adding it to the transaction. * diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java index 3b91d83ba160..b2814570cb36 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java @@ -23,12 +23,16 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletionException; import org.junit.After; import org.junit.Before; import org.junit.Test; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.VersionedRecord; @@ -145,40 +149,62 @@ public void deleteTable() { .build()).join(); } - // 1. deleteItem(T item) on Non-versioned record - // -> Optimistic Locking NOT applied -> deletes the record + + // 1. deleteItem(T item) - deprecated - on Non-versioned record + // -> Optimistic Locking NOT applied -> unconditionally deletes the record @Test - public void deleteItem_onNonVersionedRecord_doesNotApplyOptimisticLockingAndDeletesTheRecord() { - Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item"); + public void deprecatedDeleteItem_onNonVersionedRecord_skipsOptimisticLockingAndUnconditionallyDeletes() { + Record item = new Record().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); - mappedTable.putItem(item).join(); - mappedTable.deleteItem(item).join(); + mappedTable.putItem(item); + Record savedItem = mappedTable.getItem(r -> r.key(recordKey)).join(); + mappedTable.deleteItem(savedItem).join(); Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)).join(); assertThat(deletedItem).isNull(); } - // 2. deleteItem(T item) on Versioned record and versions match - // -> Optimistic Locking is applied -> deletes the record + // 2. deleteItem(T item) - deprecated - on Versioned record + // -> Optimistic Locking is not applied -> unconditionally deletes the record @Test - public void deleteItem_onVersionedRecord_whenVersionsMatch_appliesOptimisticLockingAndDeletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + public void deprecatedDeleteItem_onVersionedRecordAndMatchingVersions_skipsOptimisticLockingAndUnconditionallyDeletes() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + versionedRecordTable.deleteItem(savedItem).join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 3. deleteItem(T item) - deprecated - on Versioned record, with stale version + // -> Optimistic Locking is not applied -> unconditionally deletes the record + @Test + public void deprecatedDeleteItem_onVersionedRecordAndStaleVersion_skipsOptimisticLockingAndUnconditionallyDeletes() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item).join(); VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + // Simulate a stale version by changing the version number + savedItem.setVersion(2); versionedRecordTable.deleteItem(savedItem).join(); VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + // the item is deleted even though the version was stale because the old method does not apply optimistic locking assertThat(deletedItem).isNull(); } - // 3. deleteItem(T item, false) on Versioned record + // 4. deleteItem(T item, false) on Versioned record // -> Optimistic Locking false -> Optimistic Locking is NOT applied -> deletes the record @Test public void deleteItem_onVersionedRecord_noOptimisticLocking_deletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item).join(); @@ -196,11 +222,11 @@ public void deleteItem_onVersionedRecord_noOptimisticLocking_deletesTheRecord() assertThat(deletedItem).isNull(); } - // 4. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions match + // 5. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMatch_deletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item).join(); @@ -211,11 +237,11 @@ public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMatch_delet assertThat(deletedItem).isNull(); } - // 5. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions DO NOT match + // 6. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_doesNotDeleteTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item).join(); @@ -225,7 +251,7 @@ public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_do savedItem.setStringAttribute("Updated Item"); versionedRecordTable.updateItem(savedItem).join(); - // Try to delete with old version (version = 1) and flag=true - should fail + // Try to delete with old version (version = 1) and flag = true - should fail VersionedRecord oldVersionItem = new VersionedRecord().setId("123").setSort(10).setVersion(1); assertThatThrownBy(() -> versionedRecordTable.deleteItem(oldVersionItem, true).join()) @@ -234,11 +260,11 @@ public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_do .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 6. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match + // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item).join(); @@ -248,7 +274,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesThe DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) - .withOptimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, "version") .build(); versionedRecordTable.deleteItem(requestWithLocking).join(); @@ -257,11 +283,11 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesThe assertThat(deletedItem).isNull(); } - // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // 8. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item).join(); @@ -270,7 +296,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNot DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) - .withOptimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, "version") .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking).join()) @@ -278,12 +304,158 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNot .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } + // 9. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); - // 8. TransactWriteItems.addDeleteItem(T item) on Non-versioned record + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, "version") + .build(); + + versionedRecordTable.deleteItem(requestWithLocking).join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 10. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition + @Test + public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, "version") + .build(); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking).join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 11. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // -> does not delete the record + @Test + public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item).join(); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, "version") + .build(); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking).join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 12. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // -> does not delete the record + @Test + public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item).join(); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, "version") + .build(); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking).join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 13. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record // -> Optimistic Locking NOT applied -> deletes the record @Test public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { - Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item"); + Record item = new Record().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); mappedTable.putItem(item).join(); @@ -297,31 +469,30 @@ public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { assertThat(deletedItem).isNull(); } - // 9. TransactWriteItems.addDeleteItem(T item) on Versioned record and versions match + // 14. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test - public void transactDeleteItem_versionedRecord_versionsMatch_shouldSucceed() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimisticLockingAndDeletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item).join(); VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); enhancedClient.transactWriteItems( - TransactWriteItemsEnhancedRequest.builder() - .addDeleteItem(versionedRecordTable, savedItem) - .build()) - .join(); + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, savedItem) + .build()).join(); VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); assertThat(deletedItem).isNull(); } - // 10. TransactWriteItems.addDeleteItem(T item) on Versioned record and versions do NOT match + // 15. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test - public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_doesNotApplyOptimisticLockingAndDeletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptimisticLockingAndDeletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item).join(); @@ -330,20 +501,19 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_doesNotApp savedItem.setVersion(mismatchedVersion); enhancedClient.transactWriteItems( - TransactWriteItemsEnhancedRequest.builder() - .addDeleteItem(versionedRecordTable, savedItem) - .build()) - .join(); + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, savedItem) + .build()).join(); VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); assertThat(deletedItem).isNull(); } - // 11. TransactWriteItems with builder method on Versioned record and versions match + // 16. TransactWriteItems with builder method on Versioned record and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item).join(); @@ -353,41 +523,222 @@ public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_de TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) - .withOptimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, "version") .build(); enhancedClient.transactWriteItems( - TransactWriteItemsEnhancedRequest.builder() - .addDeleteItem(versionedRecordTable, requestWithLocking) - .build()) - .join(); + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build()).join(); VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); assertThat(deletedItem).isNull(); } - // 12. TransactWriteItems with builder method on Versioned record and versions do NOT match + // 17. TransactWriteItems with builder method on Versioned record and versions do NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item).join(); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .optimisticLocking(mismatchVersion, "version") + .build(); + + assertThatThrownBy(() -> enhancedClient + .transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build()) + .join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(((TransactionCanceledException) e.getCause()) + .cancellationReasons() + .stream() + .anyMatch(reason -> "ConditionalCheckFailed".equals(reason.code()))) + .isTrue()); + } + + // 18. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected + // -> Optimistic Locking is applied -> deletes the record + @Test + public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, "version") + .build(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build()); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 19. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition + @Test + public void transactDeleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, "version") + .build(); + + assertThatThrownBy(() -> enhancedClient + .transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build()) + .join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(((TransactionCanceledException) e.getCause()) + .cancellationReasons() + .stream() + .anyMatch(reason -> "ConditionalCheckFailed".equals(reason.code()))) + .isTrue()); + } + + + // 20. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // -> does not delete the record + @Test + public void transactDeleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, "version") + .build(); + + assertThatThrownBy(() -> enhancedClient + .transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build()) + .join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(((TransactionCanceledException) e.getCause()) + .cancellationReasons() + .stream() + .anyMatch(reason -> "ConditionalCheckFailed".equals(reason.code()))) + .isTrue()); + } + + // 21. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // -> does not delete the record + @Test + public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + versionedRecordTable.putItem(item).join(); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) - .withOptimisticLocking(mismatchVersion, "version") + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, "version") .build(); - assertThatThrownBy(() -> enhancedClient.transactWriteItems( - TransactWriteItemsEnhancedRequest.builder() - .addDeleteItem(versionedRecordTable, - requestWithLocking) - .build()) - .join()) + assertThatThrownBy(() -> enhancedClient + .transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build()) + .join()) .isInstanceOf(CompletionException.class) .satisfies(e -> assertThat(((TransactionCanceledException) e.getCause()) .cancellationReasons() @@ -395,4 +746,4 @@ public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch .anyMatch(reason -> "ConditionalCheckFailed".equals(reason.code()))) .isTrue()); } -} +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java index 1f8de825caec..1481b584de10 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java @@ -26,11 +26,15 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.After; import org.junit.Before; import org.junit.Test; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.VersionedRecord; @@ -146,40 +150,61 @@ public void deleteTable() { .build()); } - // 1. deleteItem(T item) on Non-versioned record - // -> Optimistic Locking NOT applied -> deletes the record + // 1. deleteItem(T item) - deprecated - on Non-versioned record + // -> Optimistic Locking NOT applied -> unconditionally deletes the record @Test - public void deleteItem_onNonVersionedRecord_doesNotApplyOptimisticLockingAndDeletesTheRecord() { - Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item"); + public void deprecatedDeleteItem_onNonVersionedRecord_skipsOptimisticLockingAndUnconditionallyDeletes() { + Record item = new Record().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); mappedTable.putItem(item); - mappedTable.deleteItem(item); + Record savedItem = mappedTable.getItem(r -> r.key(recordKey)); + mappedTable.deleteItem(savedItem); Record deletedItem = mappedTable.getItem(r -> r.key(recordKey)); assertThat(deletedItem).isNull(); } - // 2. deleteItem(T item) on Versioned record and versions match - // -> Optimistic Locking is applied -> deletes the record + // 2. deleteItem(T item) - deprecated - on Versioned record + // -> Optimistic Locking is not applied -> unconditionally deletes the record + @Test + public void deprecatedDeleteItem_onVersionedRecordAndMatchingVersions_skipsOptimisticLockingAndUnconditionallyDeletes() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + versionedRecordTable.deleteItem(savedItem); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 3. deleteItem(T item) - deprecated - on Versioned record, with stale version + // -> Optimistic Locking is not applied -> unconditionally deletes the record @Test - public void deleteItem_onVersionedRecord_whenVersionsMatch_appliesOptimisticLockingAndDeletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + public void deprecatedDeleteItem_onVersionedRecordAndStaleVersion_skipsOptimisticLockingAndUnconditionallyDeletes() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + // Simulate a stale version by changing the version number + savedItem.setVersion(2); versionedRecordTable.deleteItem(savedItem); VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + // the item is deleted even though the version was stale because the old method does not apply optimistic locking assertThat(deletedItem).isNull(); } - // 3. deleteItem(T item, false) on Versioned record + // 4. deleteItem(T item, false) on Versioned record // -> Optimistic Locking false -> Optimistic Locking is NOT applied -> deletes the record @Test public void deleteItem_onVersionedRecord_noOptimisticLocking_deletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); @@ -197,11 +222,11 @@ public void deleteItem_onVersionedRecord_noOptimisticLocking_deletesTheRecord() assertThat(deletedItem).isNull(); } - // 4. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions match + // 5. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMatch_deletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); @@ -212,11 +237,11 @@ public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMatch_delet assertThat(deletedItem).isNull(); } - // 5. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions DO NOT match + // 6. deleteItem(T item, true) on Versioned record with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_doesNotDeleteTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); @@ -234,11 +259,11 @@ public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_do .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 6. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match + // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); @@ -248,7 +273,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesThe DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) - .withOptimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, "version") .build(); versionedRecordTable.deleteItem(requestWithLocking); @@ -257,11 +282,11 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesThe assertThat(deletedItem).isNull(); } - // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // 8. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); @@ -270,7 +295,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNot DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) - .withOptimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, "version") .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking)) @@ -278,12 +303,158 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNot .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } + // 9. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, "version") + .build(); + + versionedRecordTable.deleteItem(requestWithLocking); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 10. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition + @Test + public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); - // 8. TransactWriteItems.addDeleteItem(T item) on Non-versioned record + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, "version") + .build(); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking)) + .isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 11. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // -> does not delete the record + @Test + public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, "version") + .build(); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking)) + .isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 12. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // -> does not delete the record + @Test + public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + DeleteItemEnhancedRequest requestWithLocking = + DeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, "version") + .build(); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking)) + .isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 13. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record // -> Optimistic Locking NOT applied -> deletes the record @Test public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { - Record item = new Record().setId("123").setSort(10).setStringAttribute("Test Item"); + Record item = new Record().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); mappedTable.putItem(item); @@ -297,11 +468,11 @@ public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { assertThat(deletedItem).isNull(); } - // 9. TransactWriteItems.addDeleteItem(T item) on Versioned record and versions match + // 14. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test - public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_doesNotApplyOptimisticLockingAndDeletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimisticLockingAndDeletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); @@ -316,11 +487,11 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_doesNotApplyO assertThat(deletedItem).isNull(); } - // 10. TransactWriteItems.addDeleteItem(T item) on Versioned record and versions do NOT match + // 15. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test - public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_doesNotApplyOptimisticLockingAndDeletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptimisticLockingAndDeletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); @@ -337,11 +508,11 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_doesNotApp assertThat(deletedItem).isNull(); } - // 11. TransactWriteItems with builder method on Versioned record and versions match + // 16. TransactWriteItems with builder method on Versioned record and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); @@ -351,7 +522,7 @@ public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_de TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) - .withOptimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, "version") .build(); enhancedClient.transactWriteItems( @@ -363,11 +534,11 @@ public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_de assertThat(deletedItem).isNull(); } - // 12. TransactWriteItems with builder method on Versioned record and versions do NOT match + // 17. TransactWriteItems with builder method on Versioned record and versions do NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { - VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item"); + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); versionedRecordTable.putItem(item); @@ -376,7 +547,185 @@ public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) - .withOptimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, "version") + .build(); + + TransactionCanceledException ex = + assertThrows(TransactionCanceledException.class, + () -> enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build())); + + assertTrue(ex.hasCancellationReasons()); + assertEquals(1, ex.cancellationReasons().size()); + assertEquals("ConditionalCheckFailed", ex.cancellationReasons().get(0).code()); + assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); + } + + // 18. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected + // -> Optimistic Locking is applied -> deletes the record + @Test + public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, "version") + .build(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build()); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 19. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition + @Test + public void transactDeleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, "version") + .build(); + + TransactionCanceledException ex = + assertThrows(TransactionCanceledException.class, + () -> enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build())); + + assertTrue(ex.hasCancellationReasons()); + assertEquals(1, ex.cancellationReasons().size()); + assertEquals("ConditionalCheckFailed", ex.cancellationReasons().get(0).code()); + assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); + } + + // 20. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // -> does not delete the record + @Test + public void transactDeleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, "version") + .build(); + + TransactionCanceledException ex = + assertThrows(TransactionCanceledException.class, + () -> enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addDeleteItem(versionedRecordTable, requestWithLocking) + .build())); + + assertTrue(ex.hasCancellationReasons()); + assertEquals(1, ex.cancellationReasons().size()); + assertEquals("ConditionalCheckFailed", ex.cancellationReasons().get(0).code()); + assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); + } + + // 21. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // -> does not delete the record + @Test + public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + + TransactDeleteItemEnhancedRequest requestWithLocking = + TransactDeleteItemEnhancedRequest.builder() + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, "version") .build(); TransactionCanceledException ex = diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java index f9baa2bc83bf..6141733f9945 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java @@ -273,13 +273,13 @@ public void hashCode_returnValuesOnConditionCheckFailure() { } @Test - public void withOptimisticLockingBuilder_addsVersionConditionExpression() { + public void optimisticLockingBuilder_addsVersionConditionExpression() { AttributeValue versionValue = AttributeValue.builder().n("1").build(); DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder() .key(Key.builder().partitionValue("id").build()) - .withOptimisticLocking(versionValue, "version") + .optimisticLocking(versionValue, "version") .build(); assertThat(request.conditionExpression(), notNullValue()); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java index a593faac4ae2..df46c2793e8f 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java @@ -17,11 +17,20 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.BDDAssertions.entry; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; - +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper.conditionallyApplyOptimisticLocking; +import static software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper.createVersionCondition; +import static software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper.getVersionAttributeName; +import static software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper.optimisticLocking; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import org.junit.Test; import software.amazon.awssdk.enhanced.dynamodb.Expression; @@ -29,13 +38,12 @@ import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordForUpdateExpressions; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors; -import software.amazon.awssdk.enhanced.dynamodb.internal.OptimisticLockingHelper; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; public class OptimisticLockingHelperTest { @Test - public void withOptimisticLocking_onDelete_addsConditionExpression() { + public void optimisticLocking_onDelete_addsConditionExpression() { Key key = Key.builder().partitionValue("id").build(); AttributeValue versionValue = AttributeValue.builder().n("1").build(); String versionAttributeName = "version"; @@ -43,20 +51,20 @@ public void withOptimisticLocking_onDelete_addsConditionExpression() { DeleteItemEnhancedRequest originalRequest = DeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - DeleteItemEnhancedRequest result = - OptimisticLockingHelper.withOptimisticLocking(originalRequest, versionValue, versionAttributeName); + DeleteItemEnhancedRequest result = optimisticLocking(originalRequest, versionValue, versionAttributeName); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); } @Test - public void withOptimisticLocking_onTransactDelete_addsConditionExpression() { + public void optimisticLocking_onTransactDelete_addsConditionExpression() { Key key = Key.builder().partitionValue("id").build(); AttributeValue versionValue = AttributeValue.builder().n("1").build(); String versionAttributeName = "version"; @@ -64,16 +72,16 @@ public void withOptimisticLocking_onTransactDelete_addsConditionExpression() { TransactDeleteItemEnhancedRequest originalRequest = TransactDeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - TransactDeleteItemEnhancedRequest result = - OptimisticLockingHelper.withOptimisticLocking(originalRequest, versionValue, versionAttributeName); + TransactDeleteItemEnhancedRequest result = optimisticLocking(originalRequest, versionValue, versionAttributeName); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); } @Test @@ -90,10 +98,10 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagFalse_returnsOriginalR DeleteItemEnhancedRequest originalRequest = DeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - DeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + DeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( originalRequest, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); @@ -114,16 +122,17 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecord DeleteItemEnhancedRequest originalRequest = DeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - DeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + DeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( originalRequest, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); } @Test @@ -140,10 +149,10 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecord DeleteItemEnhancedRequest originalRequest = DeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - DeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + DeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( originalRequest, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); @@ -166,16 +175,17 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecord DeleteItemEnhancedRequest originalRequest = DeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - DeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + DeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( originalRequest, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); } @Test @@ -192,10 +202,10 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagFalse_returnsO TransactDeleteItemEnhancedRequest originalRequest = TransactDeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - TransactDeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + TransactDeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( originalRequest, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); @@ -216,16 +226,17 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndNonVers TransactDeleteItemEnhancedRequest originalRequest = TransactDeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - TransactDeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + TransactDeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( originalRequest, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); } @Test @@ -242,10 +253,10 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersion TransactDeleteItemEnhancedRequest originalRequest = TransactDeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - TransactDeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + TransactDeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( originalRequest, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); @@ -268,16 +279,17 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersion TransactDeleteItemEnhancedRequest originalRequest = TransactDeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); - TransactDeleteItemEnhancedRequest result = OptimisticLockingHelper.conditionallyApplyOptimisticLocking( + TransactDeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( originalRequest, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); } @Test @@ -285,17 +297,18 @@ public void createVersionCondition_shouldCreateCorrectExpression() { AttributeValue versionValue = AttributeValue.builder().n("1").build(); String versionAttributeName = "version"; - Expression result = OptimisticLockingHelper.createVersionCondition(versionValue, versionAttributeName); + Expression result = createVersionCondition(versionValue, versionAttributeName); assertThat(result.expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); + assertThat(result.expressionValues()).containsExactly(entry(":version_value", versionValue)); } @Test public void createVersionCondition_nullAttributeName_throwsIllegalArgumentException_withMessage() { AttributeValue versionValue = AttributeValue.builder().n("1").build(); - assertThatThrownBy(() -> OptimisticLockingHelper.createVersionCondition(versionValue, null)) + assertThatThrownBy(() -> createVersionCondition(versionValue, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Version attribute name must not be null or empty."); } @@ -304,7 +317,7 @@ public void createVersionCondition_nullAttributeName_throwsIllegalArgumentExcept public void createVersionCondition_emptyAttributeName_throwsIllegalArgumentException_withMessage() { AttributeValue versionValue = AttributeValue.builder().n("1").build(); - assertThatThrownBy(() -> OptimisticLockingHelper.createVersionCondition(versionValue, "")) + assertThatThrownBy(() -> createVersionCondition(versionValue, "")) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Version attribute name must not be null or empty."); } @@ -313,7 +326,7 @@ public void createVersionCondition_emptyAttributeName_throwsIllegalArgumentExcep public void getVersionAttributeName_forVersionedRecord_returnsTheCorrectVersionValueFromTheTableSchema() { // versioned record TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); - Optional versionAttributeNameOpt = OptimisticLockingHelper.getVersionAttributeName(tableSchema); + Optional versionAttributeNameOpt = getVersionAttributeName(tableSchema); assertNotNull(versionAttributeNameOpt); assertTrue(versionAttributeNameOpt.isPresent()); @@ -324,14 +337,14 @@ public void getVersionAttributeName_forVersionedRecord_returnsTheCorrectVersionV public void getVersionAttributeName_forNonVersionedRecord_shouldNotReturnAVersionValue() { // non-versioned record TableSchema tableSchema = TableSchema.fromClass(RecordForUpdateExpressions.class); - Optional versionAttributeNameOpt = OptimisticLockingHelper.getVersionAttributeName(tableSchema); + Optional versionAttributeNameOpt = getVersionAttributeName(tableSchema); assertNotNull(versionAttributeNameOpt); assertFalse(versionAttributeNameOpt.isPresent()); } @Test - public void buildDeleteItemEnhancedRequest_addsCorrectExpressionOnRequest() { + public void buildDeleteItemEnhancedRequest_withOptimisticLocking_addsOptimisticLockingCondition() { Key key = Key.builder().partitionValue("id").build(); AttributeValue versionValue = AttributeValue.builder().n("1").build(); String versionAttributeName = "version"; @@ -339,13 +352,61 @@ public void buildDeleteItemEnhancedRequest_addsCorrectExpressionOnRequest() { DeleteItemEnhancedRequest result = DeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); assertThat(result.key()).isEqualTo(key); assertThat(result.conditionExpression()).isNotNull(); assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); + } + + @Test + public void buildDeleteItemEnhancedRequest_withOptimisticLockingAndCustomCondition_mergesConditions() { + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + Map expressionNames = new HashMap<>(); + expressionNames.put("#key1", "key1"); + expressionNames.put("#key2", "key2"); + + Map expressionValues = new HashMap<>(); + expressionValues.put(":value1", numberValue(10)); + expressionValues.put(":value2", numberValue(20)); + + Expression conditionExpression = + Expression.builder() + .expression("#key1 = :value1 OR #key2 = :value2") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + DeleteItemEnhancedRequest result = + DeleteItemEnhancedRequest.builder() + .key(key) + .conditionExpression(conditionExpression) + .optimisticLocking(versionValue, versionAttributeName) + .build(); + + assertThat(result.key()).isEqualTo(key); + assertThat(result.conditionExpression()).isNotNull(); + + assertThat(result.conditionExpression().expression()).isEqualTo( + "(#key1 = :value1 OR #key2 = :value2) AND (#AMZN_MAPPED_version = :version_value)"); + + Map expectedExpressionNames = new HashMap<>(); + expectedExpressionNames.put("#AMZN_MAPPED_version", "version"); + expectedExpressionNames.put("#key1", "key1"); + expectedExpressionNames.put("#key2", "key2"); + assertThat(result.conditionExpression().expressionNames()).containsExactlyInAnyOrderEntriesOf(expectedExpressionNames); + + Map expectedExpressionValues = new HashMap<>(); + expectedExpressionValues.put(":version_value", AttributeValue.builder().n("1").build()); + expectedExpressionValues.put(":value1", AttributeValue.builder().n("10").build()); + expectedExpressionValues.put(":value2", AttributeValue.builder().n("20").build()); + assertThat(result.conditionExpression().expressionValues()).containsExactlyInAnyOrderEntriesOf(expectedExpressionValues); } @Test @@ -360,12 +421,15 @@ public void buildDeleteItemEnhancedRequest_differentVersionAttributeNames_addsCo DeleteItemEnhancedRequest result = DeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, attributeName) + .optimisticLocking(versionValue, attributeName) .build(); assertThat(result.conditionExpression().expression()).isEqualTo( "#AMZN_MAPPED_" + attributeName + " = :version_value"); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_" + attributeName, attributeName)); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":version_value", versionValue)); } } @@ -385,10 +449,12 @@ public void buildDeleteItemEnhancedRequest_differentVersionValues_addsCorrectExp DeleteItemEnhancedRequest result = DeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, "version") + .optimisticLocking(versionValue, "version") .build(); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); + assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); } } @@ -401,7 +467,7 @@ public void buildDeleteItemEnhancedRequest_preservesExistingRequestProperties() DeleteItemEnhancedRequest.builder() .key(key) .returnConsumedCapacity("TOTAL") - .withOptimisticLocking(versionValue, "version") + .optimisticLocking(versionValue, "version") .build(); assertThat(result.key()).isEqualTo(key); @@ -418,13 +484,16 @@ public void buildTransactDeleteItemEnhancedRequest_addsCorrectExpressionOnReques TransactDeleteItemEnhancedRequest result = TransactDeleteItemEnhancedRequest.builder() .key(key) - .withOptimisticLocking(versionValue, versionAttributeName) + .optimisticLocking(versionValue, versionAttributeName) .build(); assertThat(result.key()).isEqualTo(key); assertThat(result.conditionExpression()).isNotNull(); assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_recordVersion = :version_value"); - assertThat(result.conditionExpression().expressionValues()).containsEntry(":version_value", versionValue); + assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_recordVersion", + "recordVersion" + )); + assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); } } From 31e5aca0ad12d5ef26b62485f7eedf0a48260184 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Thu, 26 Feb 2026 21:15:12 +0200 Subject: [PATCH 09/14] Address PR feedback --- .../internal/OptimisticLockingHelper.java | 64 +++-- .../client/DefaultDynamoDbAsyncTable.java | 6 +- .../internal/client/DefaultDynamoDbTable.java | 6 +- .../model/OptimisticLockingHelperTest.java | 234 ++++++++++-------- 4 files changed, 175 insertions(+), 135 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java index 92fa6d413683..7e2e65743275 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.internal; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef; import java.util.Collections; import java.util.Optional; @@ -41,85 +42,91 @@ private OptimisticLockingHelper() { /** * Adds optimistic locking to a delete request. * - * @param request the original delete request + * @param requestBuilder the original delete request builder * @param versionValue the expected version value * @param versionAttributeName the version attribute name * @return delete request with optimistic locking condition */ public static DeleteItemEnhancedRequest optimisticLocking( - DeleteItemEnhancedRequest request, AttributeValue versionValue, String versionAttributeName) { - - Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName); - return request.toBuilder() - .conditionExpression(conditionExpression) - .build(); + DeleteItemEnhancedRequest.Builder requestBuilder, AttributeValue versionValue, String versionAttributeName) { + return requestBuilder + .conditionExpression(createVersionCondition(versionValue, versionAttributeName)) + .build(); } /** * Adds optimistic locking to a transactional delete request. * - * @param request the original transactional delete request + * @param requestBuilder the original delete request builder * @param versionValue the expected version value * @param versionAttributeName the version attribute name * @return transactional delete request with optimistic locking condition */ public static TransactDeleteItemEnhancedRequest optimisticLocking( - TransactDeleteItemEnhancedRequest request, AttributeValue versionValue, String versionAttributeName) { + TransactDeleteItemEnhancedRequest.Builder requestBuilder, AttributeValue versionValue, String versionAttributeName) { Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName); - return request.toBuilder() - .conditionExpression(conditionExpression) - .build(); + return requestBuilder + .conditionExpression(conditionExpression) + .build(); } /** * Conditionally applies optimistic locking if enabled and version information exists. * * @param the type of the item - * @param request the original delete request + * @param requestBuilder the delete request builder * @param keyItem the item containing version information * @param tableSchema the table schema * @param useOptimisticLocking if true, applies optimistic locking * @return delete request with optimistic locking if enabled and version exists, otherwise original request */ public static DeleteItemEnhancedRequest conditionallyApplyOptimisticLocking( - DeleteItemEnhancedRequest request, T keyItem, TableSchema tableSchema, boolean useOptimisticLocking) { + DeleteItemEnhancedRequest.Builder requestBuilder, T keyItem, TableSchema tableSchema, boolean useOptimisticLocking) { if (!useOptimisticLocking) { - return request; + return requestBuilder.build(); } return getVersionAttributeName(tableSchema) .map(versionAttributeName -> { + AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName); - return version != null ? optimisticLocking(request, version, versionAttributeName) : request; + return version != null + ? optimisticLocking(requestBuilder, version, versionAttributeName) + : requestBuilder.build(); + }) - .orElse(request); + .orElseGet(requestBuilder::build); } /** * Conditionally applies optimistic locking if enabled and version information exists. * * @param the type of the item - * @param request the original transactional delete request + * @param requestBuilder the transactional delete request builder * @param keyItem the item containing version information * @param tableSchema the table schema * @param useOptimisticLocking if true, applies optimistic locking * @return delete request with optimistic locking if enabled and version exists, otherwise original request */ public static TransactDeleteItemEnhancedRequest conditionallyApplyOptimisticLocking( - TransactDeleteItemEnhancedRequest request, T keyItem, TableSchema tableSchema, boolean useOptimisticLocking) { + TransactDeleteItemEnhancedRequest.Builder requestBuilder, T keyItem, TableSchema tableSchema, + boolean useOptimisticLocking) { if (!useOptimisticLocking) { - return request; + return requestBuilder.build(); } return getVersionAttributeName(tableSchema) .map(versionAttributeName -> { AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName); - return version != null ? optimisticLocking(request, version, versionAttributeName) : request; + return version != null + ? optimisticLocking(requestBuilder, version, versionAttributeName) + : requestBuilder.build(); + }) - .orElse(request); + .orElseGet(requestBuilder::build); } @@ -129,24 +136,29 @@ public static TransactDeleteItemEnhancedRequest conditionallyApplyOptimistic * @param versionValue the expected version value * @param versionAttributeName the version attribute name * @return version check condition expression - * @throws IllegalArgumentException if {@code versionAttributeName} is null or empty + * @throws IllegalArgumentException if {@code versionAttributeName} or {@code versionValue} are null or empty */ public static Expression createVersionCondition(AttributeValue versionValue, String versionAttributeName) { if (versionAttributeName == null || versionAttributeName.trim().isEmpty()) { throw new IllegalArgumentException("Version attribute name must not be null or empty."); } + if (versionValue == null || versionValue.n() == null || versionValue.n().trim().isEmpty()) { + throw new IllegalArgumentException("Version value must not be null or empty."); + } + String attributeKeyRef = keyRef(versionAttributeName); + String attributeValueRef = valueRef(versionAttributeName); return Expression.builder() - .expression(String.format("%s = :version_value", attributeKeyRef)) - .putExpressionValue(":version_value", versionValue) + .expression(String.format("%s = %s", attributeKeyRef, attributeValueRef)) .expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeName)) + .expressionValues(Collections.singletonMap(attributeValueRef, versionValue)) .build(); } /** - * Gets the version attribute name from table schema. + * Gets the version attribute name from table schema. v * * @param the type of the item * @param tableSchema the table schema diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java index a6e10de68405..bf72a910b3f4 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java @@ -172,9 +172,9 @@ public CompletableFuture deleteItem(T keyItem) { */ @Override public CompletableFuture deleteItem(T keyItem, boolean useOptimisticLocking) { - DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)).build(); - request = conditionallyApplyOptimisticLocking(request, keyItem, tableSchema, useOptimisticLocking); - return deleteItem(request); + DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)); + conditionallyApplyOptimisticLocking(builder, keyItem, tableSchema, useOptimisticLocking); + return deleteItem(builder.build()); } @Override diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java index f4742e79f097..b2ad8819b23d 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java @@ -173,9 +173,9 @@ public T deleteItem(T keyItem) { */ @Override public T deleteItem(T keyItem, boolean useOptimisticLocking) { - DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)).build(); - request = conditionallyApplyOptimisticLocking(request, keyItem, tableSchema, useOptimisticLocking); - return deleteItem(request); + DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)); + conditionallyApplyOptimisticLocking(builder, keyItem, tableSchema, useOptimisticLocking); + return deleteItem(builder.build()); } @Override diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java index df46c2793e8f..d9f30c0dc573 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java @@ -48,19 +48,21 @@ public void optimisticLocking_onDelete_addsConditionExpression() { AttributeValue versionValue = AttributeValue.builder().n("1").build(); String versionAttributeName = "version"; - DeleteItemEnhancedRequest originalRequest = + DeleteItemEnhancedRequest.Builder originalRequestBuilder = DeleteItemEnhancedRequest.builder() .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); + .optimisticLocking(versionValue, versionAttributeName); - DeleteItemEnhancedRequest result = optimisticLocking(originalRequest, versionValue, versionAttributeName); + DeleteItemEnhancedRequest result = optimisticLocking(originalRequestBuilder, versionValue, versionAttributeName); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); - assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", versionValue)); } @Test @@ -69,19 +71,21 @@ public void optimisticLocking_onTransactDelete_addsConditionExpression() { AttributeValue versionValue = AttributeValue.builder().n("1").build(); String versionAttributeName = "version"; - TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.Builder originalRequestBuilder = TransactDeleteItemEnhancedRequest.builder() .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); + .optimisticLocking(versionValue, versionAttributeName); - TransactDeleteItemEnhancedRequest result = optimisticLocking(originalRequest, versionValue, versionAttributeName); + TransactDeleteItemEnhancedRequest result = optimisticLocking(originalRequestBuilder, versionValue, versionAttributeName); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); - assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", versionValue)); } @Test @@ -95,17 +99,16 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagFalse_returnsOriginalR RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); - DeleteItemEnhancedRequest originalRequest = + DeleteItemEnhancedRequest.Builder requestBuilder = DeleteItemEnhancedRequest.builder() .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); + .optimisticLocking(versionValue, versionAttributeName); DeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( - originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + requestBuilder, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); - assertEquals(originalRequest, result); + assertEquals(requestBuilder.build(), result); } @Test @@ -119,44 +122,22 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecord RecordForUpdateExpressions keyItem = new RecordForUpdateExpressions(); TableSchema tableSchema = TableSchema.fromClass(RecordForUpdateExpressions.class); - DeleteItemEnhancedRequest originalRequest = + DeleteItemEnhancedRequest.Builder originalRequestBuilder = DeleteItemEnhancedRequest.builder() .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); + .optimisticLocking(versionValue, versionAttributeName); DeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( - originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + originalRequestBuilder, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); - assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); - } - - @Test - public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecordWithoutVersion_returnsOriginalRequest() { - boolean optimisticLockingEnabled = true; - Key key = Key.builder().partitionValue("id").build(); - AttributeValue versionValue = null; - String versionAttributeName = "version"; - - // versioned record - RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); - TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); - - DeleteItemEnhancedRequest originalRequest = - DeleteItemEnhancedRequest.builder() - .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); - - DeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( - originalRequest, keyItem, tableSchema, optimisticLockingEnabled); - - assertThat(result).isNotNull(); - assertEquals(originalRequest, result); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", versionValue)); } @Test @@ -172,20 +153,22 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecord keyItem.setVersion(version); TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); - DeleteItemEnhancedRequest originalRequest = + DeleteItemEnhancedRequest.Builder originalRequestBuilder = DeleteItemEnhancedRequest.builder() .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); + .optimisticLocking(versionValue, versionAttributeName); DeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( - originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + originalRequestBuilder, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); - assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", versionValue)); } @Test @@ -199,17 +182,16 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagFalse_returnsO RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); - TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.Builder originalRequestBuilder = TransactDeleteItemEnhancedRequest.builder() .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); + .optimisticLocking(versionValue, versionAttributeName); TransactDeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( - originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + originalRequestBuilder, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); - assertEquals(originalRequest, result); + assertEquals(originalRequestBuilder.build(), result); } @Test @@ -223,20 +205,22 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndNonVers RecordForUpdateExpressions keyItem = new RecordForUpdateExpressions(); TableSchema tableSchema = TableSchema.fromClass(RecordForUpdateExpressions.class); - TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.Builder originalRequestBuilder = TransactDeleteItemEnhancedRequest.builder() .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); + .optimisticLocking(versionValue, versionAttributeName); TransactDeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( - originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + originalRequestBuilder, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); - assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", versionValue)); } @Test @@ -250,17 +234,16 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersion RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); - TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.Builder originalRequestBuilder = TransactDeleteItemEnhancedRequest.builder() .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); + .optimisticLocking(versionValue, versionAttributeName); TransactDeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( - originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + originalRequestBuilder, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); - assertEquals(originalRequest, result); + assertEquals(originalRequestBuilder.build(), result); } @Test @@ -276,20 +259,22 @@ public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersion keyItem.setVersion(version); TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); - TransactDeleteItemEnhancedRequest originalRequest = + TransactDeleteItemEnhancedRequest.Builder originalRequestBuilder = TransactDeleteItemEnhancedRequest.builder() .key(key) - .optimisticLocking(versionValue, versionAttributeName) - .build(); + .optimisticLocking(versionValue, versionAttributeName); TransactDeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( - originalRequest, keyItem, tableSchema, optimisticLockingEnabled); + originalRequestBuilder, keyItem, tableSchema, optimisticLockingEnabled); assertThat(result).isNotNull(); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); - assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", versionValue)); } @Test @@ -299,29 +284,64 @@ public void createVersionCondition_shouldCreateCorrectExpression() { Expression result = createVersionCondition(versionValue, versionAttributeName); - assertThat(result.expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); - assertThat(result.expressionValues()).containsExactly(entry(":version_value", versionValue)); + assertThat(result.expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + assertThat(result.expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + assertThat(result.expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", versionValue)); } @Test - public void createVersionCondition_nullAttributeName_throwsIllegalArgumentException_withMessage() { + public void createVersionCondition_nullVersionAttributeName_throwsIllegalArgumentException() { AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = null; - assertThatThrownBy(() -> createVersionCondition(versionValue, null)) + assertThatThrownBy(() -> createVersionCondition(versionValue, versionAttributeName)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Version attribute name must not be null or empty."); } @Test - public void createVersionCondition_emptyAttributeName_throwsIllegalArgumentException_withMessage() { + public void createVersionCondition_emptyVersionAttributeName_throwsIllegalArgumentException() { AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = " "; - assertThatThrownBy(() -> createVersionCondition(versionValue, "")) + assertThatThrownBy(() -> createVersionCondition(versionValue, versionAttributeName)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Version attribute name must not be null or empty."); } + @Test + public void createVersionCondition_nullVersionValue_throwsIllegalArgumentException() { + AttributeValue versionValue = null; + String versionAttributeName = "version"; + + assertThatThrownBy(() -> createVersionCondition(versionValue, versionAttributeName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Version value must not be null or empty."); + } + + @Test + public void createVersionCondition_nullVersionAttributeValue_throwsIllegalArgumentException() { + AttributeValue versionValue = AttributeValue.fromN(null); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> createVersionCondition(versionValue, versionAttributeName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Version value must not be null or empty."); + } + + @Test + public void createVersionCondition_emptyVersionAttributeValue_throwsIllegalArgumentException() { + AttributeValue versionValue = AttributeValue.fromN(" "); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> createVersionCondition(versionValue, versionAttributeName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Version value must not be null or empty."); + } + @Test public void getVersionAttributeName_forVersionedRecord_returnsTheCorrectVersionValueFromTheTableSchema() { // versioned record @@ -357,9 +377,12 @@ public void buildDeleteItemEnhancedRequest_withOptimisticLocking_addsOptimisticL assertThat(result.key()).isEqualTo(key); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); - assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", versionValue)); } @Test @@ -394,7 +417,7 @@ public void buildDeleteItemEnhancedRequest_withOptimisticLockingAndCustomConditi assertThat(result.conditionExpression()).isNotNull(); assertThat(result.conditionExpression().expression()).isEqualTo( - "(#key1 = :value1 OR #key2 = :value2) AND (#AMZN_MAPPED_version = :version_value)"); + "(#key1 = :value1 OR #key2 = :value2) AND (#AMZN_MAPPED_version = :AMZN_MAPPED_version)"); Map expectedExpressionNames = new HashMap<>(); expectedExpressionNames.put("#AMZN_MAPPED_version", "version"); @@ -403,7 +426,7 @@ public void buildDeleteItemEnhancedRequest_withOptimisticLockingAndCustomConditi assertThat(result.conditionExpression().expressionNames()).containsExactlyInAnyOrderEntriesOf(expectedExpressionNames); Map expectedExpressionValues = new HashMap<>(); - expectedExpressionValues.put(":version_value", AttributeValue.builder().n("1").build()); + expectedExpressionValues.put(":AMZN_MAPPED_version", AttributeValue.builder().n("1").build()); expectedExpressionValues.put(":value1", AttributeValue.builder().n("10").build()); expectedExpressionValues.put(":value2", AttributeValue.builder().n("20").build()); assertThat(result.conditionExpression().expressionValues()).containsExactlyInAnyOrderEntriesOf(expectedExpressionValues); @@ -425,11 +448,11 @@ public void buildDeleteItemEnhancedRequest_differentVersionAttributeNames_addsCo .build(); assertThat(result.conditionExpression().expression()).isEqualTo( - "#AMZN_MAPPED_" + attributeName + " = :version_value"); + "#AMZN_MAPPED_" + attributeName + " = :AMZN_MAPPED_" + attributeName); assertThat(result.conditionExpression().expressionNames()).containsExactly( entry("#AMZN_MAPPED_" + attributeName, attributeName)); assertThat(result.conditionExpression().expressionValues()).containsExactly( - entry(":version_value", versionValue)); + entry(":AMZN_MAPPED_" + attributeName, versionValue)); } } @@ -452,9 +475,13 @@ public void buildDeleteItemEnhancedRequest_differentVersionValues_addsCorrectExp .optimisticLocking(versionValue, "version") .build(); - assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_version = :version_value"); - assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_version", "version")); - assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", + versionValue)); } } @@ -489,11 +516,12 @@ public void buildTransactDeleteItemEnhancedRequest_addsCorrectExpressionOnReques assertThat(result.key()).isEqualTo(key); assertThat(result.conditionExpression()).isNotNull(); - assertThat(result.conditionExpression().expression()).isEqualTo("#AMZN_MAPPED_recordVersion = :version_value"); - assertThat(result.conditionExpression().expressionNames()).containsExactly(entry("#AMZN_MAPPED_recordVersion", - "recordVersion" - )); - assertThat(result.conditionExpression().expressionValues()).containsExactly(entry(":version_value", versionValue)); + assertThat(result.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_recordVersion = :AMZN_MAPPED_recordVersion"); + assertThat(result.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_recordVersion", "recordVersion")); + assertThat(result.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_recordVersion", versionValue)); } } From 48deb2862d7b02d44f5d5545a3a5de1a6770dac9 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 27 Feb 2026 13:30:14 +0200 Subject: [PATCH 10/14] Address PR feedback --- .../internal/OptimisticLockingHelper.java | 22 ++++++------ .../model/DeleteItemEnhancedRequestTest.java | 34 +++++++++++-------- .../model/OptimisticLockingHelperTest.java | 23 +++++++++++++ 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java index 7e2e65743275..b22b4862960f 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/OptimisticLockingHelper.java @@ -36,6 +36,8 @@ @SdkInternalApi public final class OptimisticLockingHelper { + private static final String CUSTOM_VERSION_METADATA_KEY = "VersionedRecordExtension:VersionAttribute"; + private OptimisticLockingHelper() { } @@ -47,8 +49,9 @@ private OptimisticLockingHelper() { * @param versionAttributeName the version attribute name * @return delete request with optimistic locking condition */ - public static DeleteItemEnhancedRequest optimisticLocking( - DeleteItemEnhancedRequest.Builder requestBuilder, AttributeValue versionValue, String versionAttributeName) { + public static DeleteItemEnhancedRequest optimisticLocking(DeleteItemEnhancedRequest.Builder requestBuilder, + AttributeValue versionValue, String versionAttributeName) { + return requestBuilder .conditionExpression(createVersionCondition(versionValue, versionAttributeName)) .build(); @@ -62,8 +65,8 @@ public static DeleteItemEnhancedRequest optimisticLocking( * @param versionAttributeName the version attribute name * @return transactional delete request with optimistic locking condition */ - public static TransactDeleteItemEnhancedRequest optimisticLocking( - TransactDeleteItemEnhancedRequest.Builder requestBuilder, AttributeValue versionValue, String versionAttributeName) { + public static TransactDeleteItemEnhancedRequest optimisticLocking(TransactDeleteItemEnhancedRequest.Builder requestBuilder, + AttributeValue versionValue, String versionAttributeName) { Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName); return requestBuilder @@ -90,14 +93,12 @@ public static DeleteItemEnhancedRequest conditionallyApplyOptimisticLocking( return getVersionAttributeName(tableSchema) .map(versionAttributeName -> { - AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName); return version != null ? optimisticLocking(requestBuilder, version, versionAttributeName) : requestBuilder.build(); - }) - .orElseGet(requestBuilder::build); + }).orElseGet(requestBuilder::build); } /** @@ -125,8 +126,7 @@ public static TransactDeleteItemEnhancedRequest conditionallyApplyOptimistic ? optimisticLocking(requestBuilder, version, versionAttributeName) : requestBuilder.build(); - }) - .orElseGet(requestBuilder::build); + }).orElseGet(requestBuilder::build); } @@ -158,13 +158,13 @@ public static Expression createVersionCondition(AttributeValue versionValue, Str } /** - * Gets the version attribute name from table schema. v + * Gets the version attribute name from table schema. * * @param the type of the item * @param tableSchema the table schema * @return version attribute name if present, empty otherwise */ public static Optional getVersionAttributeName(TableSchema tableSchema) { - return tableSchema.tableMetadata().customMetadataObject("VersionedRecordExtension:VersionAttribute", String.class); + return tableSchema.tableMetadata().customMetadataObject(CUSTOM_VERSION_METADATA_KEY, String.class); } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java index 6141733f9945..998b32d8e96a 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/DeleteItemEnhancedRequestTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.model; +import static org.assertj.core.api.BDDAssertions.entry; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -23,6 +24,7 @@ import static org.hamcrest.Matchers.nullValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -125,20 +127,20 @@ public void equals_keyNotEqual() { @Test public void equals_conditionExpressionNotEqual() { Expression conditionExpression1 = Expression.builder() - .expression("#key = :value OR #key1 = :value1") - .putExpressionName("#key", "attribute") - .putExpressionName("#key1", "attribute3") - .putExpressionValue(":value", stringValue("wrong")) - .putExpressionValue(":value1", stringValue("three")) - .build(); + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "attribute3") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); Expression conditionExpression2 = Expression.builder() - .expression("#key = :value AND #key1 = :value1") - .putExpressionName("#key", "attribute") - .putExpressionName("#key1", "attribute3") - .putExpressionValue(":value", stringValue("wrong")) - .putExpressionValue(":value1", stringValue("three")) - .build(); + .expression("#key = :value AND #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "attribute3") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); DeleteItemEnhancedRequest builtObject1 = DeleteItemEnhancedRequest.builder() .conditionExpression(conditionExpression1) @@ -283,7 +285,11 @@ public void optimisticLockingBuilder_addsVersionConditionExpression() { .build(); assertThat(request.conditionExpression(), notNullValue()); - assertThat(request.conditionExpression().expression(), is("#AMZN_MAPPED_version = :version_value")); - assertThat(request.conditionExpression().expressionValues().get(":version_value"), equalTo(versionValue)); + Assertions.assertThat(request.conditionExpression().expression()).isEqualTo( + "#AMZN_MAPPED_version = :AMZN_MAPPED_version"); + Assertions.assertThat(request.conditionExpression().expressionNames()).containsExactly( + entry("#AMZN_MAPPED_version", "version")); + Assertions.assertThat(request.conditionExpression().expressionValues()).containsExactly( + entry(":AMZN_MAPPED_version", versionValue)); } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java index d9f30c0dc573..1a9ec7f07cac 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/OptimisticLockingHelperTest.java @@ -140,6 +140,29 @@ public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecord entry(":AMZN_MAPPED_version", versionValue)); } + @Test + public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecordWithNullVersion_returnsOriginalRequest() { + boolean optimisticLockingEnabled = true; + Key key = Key.builder().partitionValue("id").build(); + AttributeValue versionValue = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + // versioned record + RecordWithUpdateBehaviors keyItem = new RecordWithUpdateBehaviors(); + TableSchema tableSchema = TableSchema.fromClass(RecordWithUpdateBehaviors.class); + + DeleteItemEnhancedRequest.Builder originalRequestBuilder = + DeleteItemEnhancedRequest.builder() + .key(key) + .optimisticLocking(versionValue, versionAttributeName); + + DeleteItemEnhancedRequest result = conditionallyApplyOptimisticLocking( + originalRequestBuilder, keyItem, tableSchema, optimisticLockingEnabled); + + assertThat(result).isNotNull(); + assertEquals(originalRequestBuilder.build(), result); + } + @Test public void conditionallyApplyOptimistic_onDelete_whenFlagTrueAndVersionedRecordWithVersion_addsConditionExpression() { boolean optimisticLockingEnabled = true; From f925da39aa940236cbaf86c00677d3555b20b911 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 27 Feb 2026 15:23:14 +0200 Subject: [PATCH 11/14] Address PR feedback --- .../OptimisticLockingAsyncCrudTest.java | 24 +++++++++---------- .../OptimisticLockingCrudTest.java | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java index b2814570cb36..a09843713087 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java @@ -263,7 +263,7 @@ public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_do // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test - public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + public void deleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -286,7 +286,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesThe // 8. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test - public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + public void deleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -307,7 +307,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNot // 9. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test - public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -345,7 +345,7 @@ public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreResp // 10. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test - public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + public void deleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -382,7 +382,7 @@ public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCus // 11. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test - public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + public void deleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -418,7 +418,7 @@ public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticCond // 12. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test - public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -512,7 +512,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptim // 16. TransactWriteItems with builder method on Versioned record and versions match // -> Optimistic Locking is applied -> deletes the record @Test - public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -538,7 +538,7 @@ public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_de // 17. TransactWriteItems with builder method on Versioned record and versions do NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test - public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -568,7 +568,7 @@ public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch // 18. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test - public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -610,7 +610,7 @@ public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditio // 19. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test - public void transactDeleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -658,7 +658,7 @@ public void transactDeleteItemWithBuilder_whenOptimisticLockingConditionRespecte // 20. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test - public void transactDeleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -704,7 +704,7 @@ public void transactDeleteItemWithBuilder_whenCustomConditionRespected_butOptimi // 21. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test - public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java index 1481b584de10..7afcd3c4b017 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java @@ -262,7 +262,7 @@ public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_do // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test - public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + public void deleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -285,7 +285,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesThe // 8. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test - public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + public void deleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -306,7 +306,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNot // 9. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test - public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -344,7 +344,7 @@ public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreResp // 10. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test - public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + public void deleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -381,7 +381,7 @@ public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCus // 11. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test - public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + public void deleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -417,7 +417,7 @@ public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticCond // 12. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test - public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -511,7 +511,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptim // 16. TransactWriteItems with builder method on Versioned record and versions match // -> Optimistic Locking is applied -> deletes the record @Test - public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -537,7 +537,7 @@ public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_de // 17. TransactWriteItems with builder method on Versioned record and versions do NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test - public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -566,7 +566,7 @@ public void transactDeleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch // 18. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test - public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -608,7 +608,7 @@ public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditio // 19. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test - public void transactDeleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -654,7 +654,7 @@ public void transactDeleteItemWithBuilder_whenOptimisticLockingConditionRespecte // 20. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test - public void transactDeleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -699,7 +699,7 @@ public void transactDeleteItemWithBuilder_whenCustomConditionRespected_butOptimi // 21. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test - public void transactDeleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); From f241c812f15aeb1b21ee112316ceec7087b43635 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 27 Feb 2026 16:56:43 +0200 Subject: [PATCH 12/14] Address PR feedback --- .../OptimisticLockingAsyncCrudTest.java | 249 ++++++++++++++++-- .../OptimisticLockingCrudTest.java | 244 +++++++++++++++-- 2 files changed, 451 insertions(+), 42 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java index a09843713087..948041f4e32a 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java @@ -271,10 +271,12 @@ public void deleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesThe VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); versionedRecordTable.deleteItem(requestWithLocking).join(); @@ -293,10 +295,12 @@ public void deleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNot versionedRecordTable.putItem(item).join(); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking).join()) @@ -329,11 +333,13 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreResp .build(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); versionedRecordTable.deleteItem(requestWithLocking).join(); @@ -367,11 +373,13 @@ public void deleteItemWithRequest_whenOptimisticLockingConditionRespected_butCus .build(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking).join()) @@ -403,11 +411,13 @@ public void deleteItemWithRequest_whenCustomConditionRespected_butOptimisticCond versionedRecordTable.putItem(item).join(); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking).join()) @@ -439,11 +449,13 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotResp versionedRecordTable.putItem(item).join(); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking).join()) @@ -451,7 +463,195 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotResp .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 13. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record + // 13. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + AttributeValue matchVersion = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + versionedRecordTable.putItem(item); + + versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .optimisticLocking(matchVersion, versionAttributeName)) + .join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 14. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // -> Optimistic Locking is applied -> does NOT delete the record + @Test + public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .optimisticLocking(mismatchVersion, versionAttributeName)) + .join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getCause()).isInstanceOf(ConditionalCheckFailedException.class)) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + + } + + // 15. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition respected + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + + versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .optimisticLocking(matchVersion, versionAttributeName) + .conditionExpression(conditionExpression)) + .join(); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + assertThat(deletedItem).isNull(); + } + + // 16. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition not respected + // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition + @Test + public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, versionAttributeName)) + .join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getCause()).isInstanceOf(ConditionalCheckFailedException.class)) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 17. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition respected + // -> does not delete the record + @Test + public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, versionAttributeName)).join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getCause()).isInstanceOf(ConditionalCheckFailedException.class)) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 18. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition fails + // -> does not delete the record + @Test + public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, versionAttributeName)) + .join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getCause()).isInstanceOf(ConditionalCheckFailedException.class)) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 19. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record // -> Optimistic Locking NOT applied -> deletes the record @Test public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { @@ -469,7 +669,7 @@ public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { assertThat(deletedItem).isNull(); } - // 14. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match + // 20. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimisticLockingAndDeletesTheRecord() { @@ -488,7 +688,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimist assertThat(deletedItem).isNull(); } - // 15. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match + // 21. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptimisticLockingAndDeletesTheRecord() { @@ -509,7 +709,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptim assertThat(deletedItem).isNull(); } - // 16. TransactWriteItems with builder method on Versioned record and versions match + // 22. TransactWriteItems with builder method on Versioned record and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { @@ -520,10 +720,12 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_de VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); enhancedClient.transactWriteItems( @@ -535,7 +737,7 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_de assertThat(deletedItem).isNull(); } - // 17. TransactWriteItems with builder method on Versioned record and versions do NOT match + // 23. TransactWriteItems with builder method on Versioned record and versions do NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { @@ -545,10 +747,12 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch versionedRecordTable.putItem(item).join(); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> enhancedClient @@ -590,12 +794,13 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditio .build(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); enhancedClient.transactWriteItems( @@ -607,7 +812,7 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditio assertThat(deletedItem).isNull(); } - // 19. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // 25. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { @@ -632,12 +837,13 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespecte .build(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> enhancedClient @@ -654,8 +860,7 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespecte .isTrue()); } - - // 20. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // 26. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { @@ -679,12 +884,13 @@ public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimi versionedRecordTable.putItem(item); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> enhancedClient @@ -701,7 +907,7 @@ public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimi .isTrue()); } - // 21. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // 27. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { @@ -725,12 +931,13 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditio versionedRecordTable.putItem(item).join(); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> enhancedClient diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java index 7afcd3c4b017..97f205a61ba7 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java @@ -270,10 +270,12 @@ public void deleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesThe VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); versionedRecordTable.deleteItem(requestWithLocking); @@ -292,10 +294,12 @@ public void deleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNot versionedRecordTable.putItem(item); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking)) @@ -328,11 +332,13 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreResp .build(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); versionedRecordTable.deleteItem(requestWithLocking); @@ -366,11 +372,13 @@ public void deleteItemWithRequest_whenOptimisticLockingConditionRespected_butCus .build(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking)) @@ -402,11 +410,13 @@ public void deleteItemWithRequest_whenCustomConditionRespected_butOptimisticCond versionedRecordTable.putItem(item); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking)) @@ -438,11 +448,13 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotResp versionedRecordTable.putItem(item); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); assertThatThrownBy(() -> versionedRecordTable.deleteItem(requestWithLocking)) @@ -450,7 +462,189 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotResp .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 13. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record + // 13. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions match + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + AttributeValue matchVersion = AttributeValue.builder().n("1").build(); + String versionAttributeName = "version"; + + versionedRecordTable.putItem(item); + + versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .optimisticLocking(matchVersion, versionAttributeName)); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 14. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // -> Optimistic Locking is applied -> does NOT delete the record + @Test + public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .optimisticLocking(mismatchVersion, versionAttributeName)) + + ).isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 15. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition respected + // -> Optimistic Locking is applied -> deletes the record + @Test + public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + + versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .optimisticLocking(matchVersion, versionAttributeName) + .conditionExpression(conditionExpression)); + + VersionedRecord deletedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + assertThat(deletedItem).isNull(); + } + + // 16. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition not respected + // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition + @Test + public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + versionedRecordTable.putItem(item); + VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(matchVersion, versionAttributeName)) + + ).isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 17. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition respected + // -> does not delete the record + @Test + public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String matchingDatabaseValue = "test"; + expressionValues.put(":value", AttributeValue.fromS(matchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, versionAttributeName)) + + ).isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 18. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition fails + // -> does not delete the record + @Test + public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); + + Map expressionNames = new HashMap<>(); + expressionNames.put("#stringAttribute", "stringAttribute"); + + Map expressionValues = new HashMap<>(); + String nonMatchingDatabaseValue = "nonMatchingValue"; + expressionValues.put(":value", AttributeValue.fromS(nonMatchingDatabaseValue)); + + Expression conditionExpression = + Expression.builder() + .expression("#stringAttribute = :value") + .expressionNames(Collections.unmodifiableMap(expressionNames)) + .expressionValues(Collections.unmodifiableMap(expressionValues)) + .build(); + + versionedRecordTable.putItem(item); + + AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(r -> r + .key(recordKey) + .conditionExpression(conditionExpression) + .optimisticLocking(mismatchVersion, versionAttributeName)) + + ).isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 19. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record // -> Optimistic Locking NOT applied -> deletes the record @Test public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { @@ -468,7 +662,7 @@ public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { assertThat(deletedItem).isNull(); } - // 14. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match + // 20. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimisticLockingAndDeletesTheRecord() { @@ -487,7 +681,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimist assertThat(deletedItem).isNull(); } - // 15. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match + // 21. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptimisticLockingAndDeletesTheRecord() { @@ -508,7 +702,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptim assertThat(deletedItem).isNull(); } - // 16. TransactWriteItems with builder method on Versioned record and versions match + // 22. TransactWriteItems with builder method on Versioned record and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { @@ -519,10 +713,12 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_de VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; + TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); enhancedClient.transactWriteItems( @@ -534,7 +730,7 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_de assertThat(deletedItem).isNull(); } - // 17. TransactWriteItems with builder method on Versioned record and versions do NOT match + // 23. TransactWriteItems with builder method on Versioned record and versions do NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { @@ -544,10 +740,12 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch versionedRecordTable.putItem(item); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; + TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); TransactionCanceledException ex = @@ -563,7 +761,7 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); } - // 18. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected + // 24. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { @@ -588,12 +786,13 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditio .build(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); enhancedClient.transactWriteItems( @@ -605,7 +804,7 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditio assertThat(deletedItem).isNull(); } - // 19. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // 25. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { @@ -630,12 +829,13 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespecte .build(); AttributeValue matchVersion = AttributeValue.builder().n(savedItem.getVersion().toString()).build(); + String versionAttributeName = "version"; TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(matchVersion, "version") + .optimisticLocking(matchVersion, versionAttributeName) .build(); TransactionCanceledException ex = @@ -651,7 +851,7 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespecte assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); } - // 20. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // 26. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { @@ -675,12 +875,13 @@ public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimi versionedRecordTable.putItem(item); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); TransactionCanceledException ex = @@ -696,7 +897,7 @@ public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimi assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); } - // 21. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // 27. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { @@ -720,12 +921,13 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditio versionedRecordTable.putItem(item); AttributeValue mismatchVersion = AttributeValue.builder().n("2").build(); + String versionAttributeName = "version"; TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest.builder() .key(recordKey) .conditionExpression(conditionExpression) - .optimisticLocking(mismatchVersion, "version") + .optimisticLocking(mismatchVersion, versionAttributeName) .build(); TransactionCanceledException ex = From efa95217e84406aca9ddb7dce1d447928de63d47 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Sun, 1 Mar 2026 13:52:50 +0200 Subject: [PATCH 13/14] Address PR feedback --- .../OptimisticLockingAsyncCrudTest.java | 12 ++++++------ .../functionaltests/OptimisticLockingCrudTest.java | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java index 948041f4e32a..e682a78db297 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java @@ -466,7 +466,7 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotResp // 13. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test - public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -487,7 +487,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesThe // 14. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test - public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -509,7 +509,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNot // 15. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test - public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -546,7 +546,7 @@ public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreResp // 16. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test - public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + public void deleteItemWithConsumer_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -583,7 +583,7 @@ public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCus // 17. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test - public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + public void deleteItemWithConsumer_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -618,7 +618,7 @@ public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticCond // 18. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test - public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java index 97f205a61ba7..5821455ce2d8 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java @@ -465,7 +465,7 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotResp // 13. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test - public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { + public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -485,7 +485,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMatch_deletesThe // 14. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test - public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { + public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -505,7 +505,7 @@ public void deleteItemWithBuilder_onVersionedRecord_whenVersionsMismatch_doesNot // 15. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test - public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { + public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -541,7 +541,7 @@ public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionAreResp // 16. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test - public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { + public void deleteItemWithConsumer_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -577,7 +577,7 @@ public void deleteItemWithBuilder_whenOptimisticLockingConditionRespected_butCus // 17. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test - public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { + public void deleteItemWithConsumer_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); @@ -612,7 +612,7 @@ public void deleteItemWithBuilder_whenCustomConditionRespected_butOptimisticCond // 18. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test - public void deleteItemWithBuilder_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { + public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); From 2176326f6523a91a0ac6f94fbe4ecad8f94a9a7f Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Sun, 1 Mar 2026 14:35:21 +0200 Subject: [PATCH 14/14] Address PR feedback --- .../OptimisticLockingAsyncCrudTest.java | 59 ++++++++++++------- .../OptimisticLockingCrudTest.java | 56 +++++++++++------- 2 files changed, 72 insertions(+), 43 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java index e682a78db297..b8d69ef12b99 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingAsyncCrudTest.java @@ -260,7 +260,22 @@ public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_do .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match + // 7. deleteItem(T item, true) on Versioned record with Optimistic Locking true, but item not found in DB + // -> Optimistic Locking is applied -> does NOT delete the record + @Test + public void deleteItem_onVersionedRecord_optimisticLockingButItemNotFoundInDb_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + versionedRecordTable.putItem(item); + + VersionedRecord nonExistingItem = new VersionedRecord().setId("123").setSort(20).setStringAttribute("test").setVersion(1); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(nonExistingItem, true).join()) + .isInstanceOf(CompletionException.class) + .satisfies(e -> assertThat(e.getCause()).isInstanceOf(ConditionalCheckFailedException.class)) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 8. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { @@ -285,7 +300,7 @@ public void deleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesThe assertThat(deletedItem).isNull(); } - // 8. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // 9. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test public void deleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { @@ -308,7 +323,7 @@ public void deleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNot .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 9. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected + // 10. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { @@ -348,7 +363,7 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreResp assertThat(deletedItem).isNull(); } - // 10. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // 11. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test public void deleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { @@ -387,7 +402,7 @@ public void deleteItemWithRequest_whenOptimisticLockingConditionRespected_butCus .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 11. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // 12. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test public void deleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { @@ -425,7 +440,7 @@ public void deleteItemWithRequest_whenCustomConditionRespected_butOptimisticCond .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 12. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // 13. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { @@ -463,7 +478,7 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotResp .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 13. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions match + // 14. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { @@ -484,7 +499,7 @@ public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMatch_deletesTh assertThat(deletedItem).isNull(); } - // 14. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // 15. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { @@ -506,7 +521,7 @@ public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMismatch_doesNo } - // 15. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition respected + // 16. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { @@ -543,7 +558,7 @@ public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionAreRes assertThat(deletedItem).isNull(); } - // 16. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition not respected + // 17. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test public void deleteItemWithConsumer_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { @@ -580,7 +595,7 @@ public void deleteItemWithConsumer_whenOptimisticLockingConditionRespected_butCu .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 17. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition respected + // 18. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test public void deleteItemWithConsumer_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { @@ -615,7 +630,7 @@ public void deleteItemWithConsumer_whenCustomConditionRespected_butOptimisticCon .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 18. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition fails + // 19. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { @@ -651,7 +666,7 @@ public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionNotRes .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 19. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record + // 20. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record // -> Optimistic Locking NOT applied -> deletes the record @Test public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { @@ -669,7 +684,7 @@ public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { assertThat(deletedItem).isNull(); } - // 20. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match + // 21. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimisticLockingAndDeletesTheRecord() { @@ -688,7 +703,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimist assertThat(deletedItem).isNull(); } - // 21. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match + // 22. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptimisticLockingAndDeletesTheRecord() { @@ -709,7 +724,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptim assertThat(deletedItem).isNull(); } - // 22. TransactWriteItems with builder method on Versioned record and versions match + // 23. TransactWriteItems with builder method on Versioned record and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { @@ -737,7 +752,7 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_de assertThat(deletedItem).isNull(); } - // 23. TransactWriteItems with builder method on Versioned record and versions do NOT match + // 24. TransactWriteItems with builder method on Versioned record and versions do NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { @@ -769,7 +784,7 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch .isTrue()); } - // 18. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected + // 25. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { @@ -812,14 +827,14 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditio assertThat(deletedItem).isNull(); } - // 25. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // 26. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); Key recordKey = Key.builder().partitionValue(item.getId()).sortValue(item.getSort()).build(); - versionedRecordTable.putItem(item); + versionedRecordTable.putItem(item).join(); VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey)).join(); Map expressionNames = new HashMap<>(); @@ -860,7 +875,7 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespecte .isTrue()); } - // 26. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // 27. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { @@ -907,7 +922,7 @@ public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimi .isTrue()); } - // 27. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // 28. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java index 5821455ce2d8..3421e63cf708 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/OptimisticLockingCrudTest.java @@ -259,7 +259,21 @@ public void deleteItem_onVersionedRecord_optimisticLockingAndVersionsMismatch_do .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 7. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match + // 7. deleteItem(T item, true) on Versioned record with Optimistic Locking true, but item not found in DB + // -> Optimistic Locking is applied -> does NOT delete the record + @Test + public void deleteItem_onVersionedRecord_optimisticLockingButItemNotFoundInDb_doesNotDeleteTheRecord() { + VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("test"); + versionedRecordTable.putItem(item); + + VersionedRecord nonExistingItem = new VersionedRecord().setId("123").setSort(20).setStringAttribute("test").setVersion(1); + + assertThatThrownBy(() -> versionedRecordTable.deleteItem(nonExistingItem, true)) + .isInstanceOf(ConditionalCheckFailedException.class) + .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); + } + + // 8. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { @@ -284,7 +298,7 @@ public void deleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesThe assertThat(deletedItem).isNull(); } - // 8. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // 9. deleteItem(DeleteItemEnhancedRequest) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test public void deleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { @@ -307,7 +321,7 @@ public void deleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNot .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 9. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected + // 10. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { @@ -347,7 +361,7 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreResp assertThat(deletedItem).isNull(); } - // 10. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // 11. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test public void deleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { @@ -386,7 +400,7 @@ public void deleteItemWithRequest_whenOptimisticLockingConditionRespected_butCus .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 11. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // 12. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test public void deleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { @@ -424,7 +438,7 @@ public void deleteItemWithRequest_whenCustomConditionRespected_butOptimisticCond .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 12. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // 13. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { @@ -462,7 +476,7 @@ public void deleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotResp .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 13. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions match + // 14. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { @@ -482,7 +496,7 @@ public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMatch_deletesTh assertThat(deletedItem).isNull(); } - // 14. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions DO NOT match + // 15. deleteItem(Consumer<>) on VersionedRecord with Optimistic Locking true and versions DO NOT match // -> Optimistic Locking is applied -> does NOT delete the record @Test public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { @@ -502,7 +516,7 @@ public void deleteItemWithConsumer_onVersionedRecord_whenVersionsMismatch_doesNo .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 15. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition respected + // 16. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { @@ -538,7 +552,7 @@ public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionAreRes assertThat(deletedItem).isNull(); } - // 16. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition not respected + // 17. deleteItem(Consumer<>) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test public void deleteItemWithConsumer_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { @@ -574,7 +588,7 @@ public void deleteItemWithConsumer_whenOptimisticLockingConditionRespected_butCu .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 17. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition respected + // 18. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test public void deleteItemWithConsumer_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { @@ -609,7 +623,7 @@ public void deleteItemWithConsumer_whenCustomConditionRespected_butOptimisticCon .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 18. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition fails + // 19. deleteItem(Consumer<>) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() { @@ -644,7 +658,7 @@ public void deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionNotRes .satisfies(e -> assertThat(e.getMessage()).contains("The conditional request failed")); } - // 19. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record + // 20. TransactWriteItems.deleteItem(T item) - deprecated - on Non-versioned record // -> Optimistic Locking NOT applied -> deletes the record @Test public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { @@ -662,7 +676,7 @@ public void transactDeleteItem_onNonVersionedRecord_deletesTheRecord() { assertThat(deletedItem).isNull(); } - // 20. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match + // 21. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimisticLockingAndDeletesTheRecord() { @@ -681,7 +695,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMatch_skipsOptimist assertThat(deletedItem).isNull(); } - // 21. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match + // 22. TransactWriteItems.deleteItem(T item) - deprecated - on Versioned record and versions do NOT match // -> Optimistic Locking is NOT applied (old deprecated method -> does NOT support Optimistic Locking) -> deletes the record @Test public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptimisticLockingAndDeletesTheRecord() { @@ -702,7 +716,7 @@ public void transactDeleteItem_onVersionedRecord_whenVersionsMismatch_skipsOptim assertThat(deletedItem).isNull(); } - // 22. TransactWriteItems with builder method on Versioned record and versions match + // 23. TransactWriteItems with builder method on Versioned record and versions match // -> Optimistic Locking is applied -> deletes the record @Test public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_deletesTheRecord() { @@ -730,7 +744,7 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMatch_de assertThat(deletedItem).isNull(); } - // 23. TransactWriteItems with builder method on Versioned record and versions do NOT match + // 24. TransactWriteItems with builder method on Versioned record and versions do NOT match // -> Optimistic Locking applied -> does NOT delete the record @Test public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord() { @@ -761,7 +775,7 @@ public void transactDeleteItemWithRequest_onVersionedRecord_whenVersionsMismatch assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); } - // 24. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected + // 25. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition respected // -> Optimistic Locking is applied -> deletes the record @Test public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord() { @@ -804,7 +818,7 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditio assertThat(deletedItem).isNull(); } - // 25. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected + // 26. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions match + custom condition not respected // -> Optimistic Locking is applied -> does not delete the record because of the failing custom condition @Test public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord() { @@ -851,7 +865,7 @@ public void transactDeleteItemWithRequest_whenOptimisticLockingConditionRespecte assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); } - // 26. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected + // 27. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition respected // -> does not delete the record @Test public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord() { @@ -897,7 +911,7 @@ public void transactDeleteItemWithRequest_whenCustomConditionRespected_butOptimi assertEquals("The conditional request failed", ex.cancellationReasons().get(0).message()); } - // 27. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails + // 28. deleteItem(DeleteItemEnhancedRequest) with Optimistic Locking true, versions do not match + custom condition fails // -> does not delete the record @Test public void transactDeleteItemWithRequest_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord() {