Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,22 @@ default CompletableFuture<T> 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<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 a CompletableFuture containing the deleted item, or null if the item was not found
*/
default CompletableFuture<T> deleteItem(T keyItem, boolean useOptimisticLocking) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing Javadoc ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added JavaDoc for this method.

throw new UnsupportedOperationException();
}

/**
* Deletes a single item from the mapped table using a supplied primary {@link Key}.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,22 @@ 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();
}

/**
* 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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add JavaDoc for this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added JavaDoc for this method.

throw new UnsupportedOperationException();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the pr description and test checklist show batchWriteItem as covered, but there's no code change to WriteBatch.Builder.addDeleteItem() or any batch-related class ? do we support it or not ?

Copy link
Contributor Author

@anasatirbasa anasatirbasa Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batch operations do not support condition expressions, so batchWriteItem is not applicable here. I’ve also removed it from the test coverage checklist in the PR.

The DynamoDB documentation confirms this:
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/BestPractices_ConditionalBatchUpdate.html

It states that you cannot specify conditions on individual put and delete requests within a batch - “you cannot specify conditions on individual put and delete requests” in a BatchWriteItem request.

}

/**
* Deletes a single item from the mapped table using a supplied primary {@link Key}.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* 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.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;
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;

/**
* Utility class for adding optimistic locking to DynamoDB delete operations.
* <p>
* 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}.
*/
@SdkInternalApi
public final class OptimisticLockingHelper {

private static final String CUSTOM_VERSION_METADATA_KEY = "VersionedRecordExtension:VersionAttribute";

private OptimisticLockingHelper() {
}

/**
* Adds optimistic locking to a 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.Builder requestBuilder,
AttributeValue versionValue, String versionAttributeName) {

return requestBuilder
.conditionExpression(createVersionCondition(versionValue, versionAttributeName))
.build();
}

/**
* Adds optimistic locking to a 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.Builder requestBuilder,
AttributeValue versionValue, String versionAttributeName) {

Expression conditionExpression = createVersionCondition(versionValue, versionAttributeName);
return requestBuilder
.conditionExpression(conditionExpression)
.build();
}

/**
* Conditionally applies optimistic locking if enabled and version information exists.
*
* @param <T> the type of the item
* @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 <T> DeleteItemEnhancedRequest conditionallyApplyOptimisticLocking(
DeleteItemEnhancedRequest.Builder requestBuilder, T keyItem, TableSchema<T> tableSchema, boolean useOptimisticLocking) {

if (!useOptimisticLocking) {
return requestBuilder.build();
}

return getVersionAttributeName(tableSchema)
.map(versionAttributeName -> {
AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName);
return version != null
? optimisticLocking(requestBuilder, version, versionAttributeName)
: requestBuilder.build();

}).orElseGet(requestBuilder::build);
}

/**
* Conditionally applies optimistic locking if enabled and version information exists.
*
* @param <T> the type of the item
* @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 <T> TransactDeleteItemEnhancedRequest conditionallyApplyOptimisticLocking(
TransactDeleteItemEnhancedRequest.Builder requestBuilder, T keyItem, TableSchema<T> tableSchema,
boolean useOptimisticLocking) {

if (!useOptimisticLocking) {
return requestBuilder.build();
}

return getVersionAttributeName(tableSchema)
.map(versionAttributeName -> {
AttributeValue version = tableSchema.attributeValue(keyItem, versionAttributeName);
return version != null
? optimisticLocking(requestBuilder, version, versionAttributeName)
: requestBuilder.build();

}).orElseGet(requestBuilder::build);
}


/**
* Creates a version condition expression.
*
* @param versionValue the expected version value
* @param versionAttributeName the version attribute name
* @return version check condition expression
* @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()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we checking the versionAttributeName but not versionValue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check for "versionValue" also:

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.");
}

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 = %s", attributeKeyRef, attributeValueRef))
.expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeName))
.expressionValues(Collections.singletonMap(attributeValueRef, versionValue))
.build();
}

/**
* Gets the version attribute name from table schema.
*
* @param <T> the type of the item
* @param tableSchema the table schema
* @return version attribute name if present, empty otherwise
*/
public static <T> Optional<String> getVersionAttributeName(TableSchema<T> tableSchema) {
return tableSchema.tableMetadata().customMetadataObject(CUSTOM_VERSION_METADATA_KEY, String.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.internal.OptimisticLockingHelper.conditionallyApplyOptimisticLocking;

import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
Expand All @@ -26,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;
Expand Down Expand Up @@ -124,30 +126,57 @@ public CompletableFuture<Void> createTable() {
.build());
}

/**
* Supports optimistic locking via {@link OptimisticLockingHelper}.
*/
@Override
public CompletableFuture<T> deleteItem(DeleteItemEnhancedRequest request) {
TableOperation<T, ?, ?, DeleteItemEnhancedResponse<T>> operation = DeleteItemOperation.create(request);
return operation.executeOnPrimaryIndexAsync(tableSchema, tableName, extension, dynamoDbClient)
.thenApply(DeleteItemEnhancedResponse::attributes);
}

/**
* Supports optimistic locking via {@link OptimisticLockingHelper}.
*/
@Override
public CompletableFuture<T> deleteItem(Consumer<DeleteItemEnhancedRequest.Builder> requestConsumer) {
DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder();
requestConsumer.accept(builder);
return deleteItem(builder.build());
}

/**
* Does not support optimistic locking. Use {@link #deleteItem(Object, boolean)} for optimistic locking support.
*/
@Override
public CompletableFuture<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 CompletableFuture<T> deleteItem(T keyItem) {
return deleteItem(keyFrom(keyItem));
}

/**
* 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<T> deleteItem(T keyItem, boolean useOptimisticLocking) {
DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem));
conditionallyApplyOptimisticLocking(builder, keyItem, tableSchema, useOptimisticLocking);
return deleteItem(builder.build());
}

@Override
public CompletableFuture<DeleteItemEnhancedResponse<T>> deleteItemWithResponse(DeleteItemEnhancedRequest request) {
TableOperation<T, ?, ?, DeleteItemEnhancedResponse<T>> operation = DeleteItemOperation.create(request);
Expand Down Expand Up @@ -311,7 +340,7 @@ public CompletableFuture<T> updateItem(T item) {
public Key keyFrom(T item) {
return createKeyFromItem(item, tableSchema, TableMetadata.primaryIndexName());
}


@Override
public CompletableFuture<Void> deleteTable() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.internal.OptimisticLockingHelper.conditionallyApplyOptimisticLocking;

import java.util.ArrayList;
import java.util.function.Consumer;
Expand All @@ -25,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;
Expand Down Expand Up @@ -126,29 +128,56 @@ public void createTable() {
.build());
}

/**
* Supports optimistic locking via {@link OptimisticLockingHelper}.
*/
@Override
public T deleteItem(DeleteItemEnhancedRequest request) {
TableOperation<T, ?, ?, DeleteItemEnhancedResponse<T>> operation = DeleteItemOperation.create(request);
return operation.executeOnPrimaryIndex(tableSchema, tableName, extension, dynamoDbClient).attributes();
}

/**
* Supports optimistic locking via {@link OptimisticLockingHelper}.
*/
@Override
public T deleteItem(Consumer<DeleteItemEnhancedRequest.Builder> requestConsumer) {
DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder();
requestConsumer.accept(builder);
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));
}

/**
* 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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should optimistic locking not follow the same experience as UpdateItem/PutItem, where the user passes the entire item? I believe its useful to keep this version, but also align the dev experience with existing API's

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is for backwards compatability, perhaps its cleaner to do this at the annotation level, rather than changing the API signature:

@DynamoDbVersionAttribute(useVersionOnDelete = true)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also prevent the "boolean trap".

DeleteItemEnhancedRequest.Builder builder = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem));
conditionallyApplyOptimisticLocking(builder, keyItem, tableSchema, useOptimisticLocking);
return deleteItem(builder.build());
}

@Override
public DeleteItemEnhancedResponse<T> deleteItemWithResponse(DeleteItemEnhancedRequest request) {
TableOperation<T, ?, ?, DeleteItemEnhancedResponse<T>> operation = DeleteItemOperation.create(request);
Expand Down
Loading