Skip to content

Optimistic Locking for Delete Operations#6747

Open
anasatirbasa wants to merge 14 commits intoaws:masterfrom
anasatirbasa:feature/optimistic-locking-for-delete-operations
Open

Optimistic Locking for Delete Operations#6747
anasatirbasa wants to merge 14 commits intoaws:masterfrom
anasatirbasa:feature/optimistic-locking-for-delete-operations

Conversation

@anasatirbasa
Copy link
Contributor

@anasatirbasa anasatirbasa commented Feb 23, 2026

Description

Adds explicit optimistic locking support for delete operations in the DynamoDB Enhanced Client, while preserving full backward compatibility.

Until now, deleteItem operations did not automatically apply version-based conditional checks, even when a record was annotated with @DynamoDbVersionAttribute. This created an inconsistency with putItem and updateItem, which already integrate optimistic locking via VersionedRecordExtension.

Default behavior remains unchanged.
Deletes are still unconditional unless optimistic locking is explicitly enabled.


Motivation and Context

This PR addresses community request #2358, where users expected delete operations to respect optimistic locking semantics similar to put/update operations.

An earlier attempt (PR #6043) proposed implicitly enabling version checks on deletes. However, this risked breaking existing applications that relied on unconditional deletes even when using @DynamoDbVersionAttribute.

To preserve backward compatibility, this PR introduces explicit opt-in optimistic locking.


What This PR Introduces

1. Boolean Overloads (Sync & Async)

New overloads allow explicit control of optimistic locking behavior:

  • T deleteItem(T keyItem, boolean useOptimisticLocking);
  • CompletableFuture<T> deleteItem(T keyItem, boolean useOptimisticLocking);

Usage:

// Unconditional delete (default behavior)
table.deleteItem(item, false);

// Optimistic locking enabled
table.deleteItem(item, true);

The existing deleteItem(T keyItem) method is now deprecated and delegates to deleteItem(keyItem, false).


2. Fluent Builder API

Added discoverable builder methods:

  • DeleteItemEnhancedRequest.Builder#withOptimisticLocking(...)
  • TransactDeleteItemEnhancedRequest.Builder#withOptimisticLocking(...)

Example:

DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder()
    .key(itemKey)
    .withOptimisticLocking(versionValue, "version")
    .build();

table.deleteItem(request);

3. Centralized Helper: OptimisticLockingHelper

A new @SdkPublicApi class that:

  • Builds the version condition expression:
    <versionAttributeName> = :version_value
  • Merges version conditions with existing conditions using AND
  • Applies locking only when:
    • useOptimisticLocking == true
    • The table schema contains a version attribute
    • The provided item has a non-null version value

If any of these conditions are not met, the delete proceeds unconditionally.


Modifications

  • Added new overload deleteItem(T keyItem, boolean useOptimisticLocking) (Sync & Async)
  • Deprecated implicit deleteItem(T keyItem) methods
  • Added .withOptimisticLocking(...) to request builders
  • Introduced OptimisticLockingHelper
  • Implemented safe condition merging logic
  • Added unit and integration tests
  • Ensured compatibility with VersionedRecordExtension
  • Preserved existing API contracts

Testing

Includes:

  • Unit tests for condition generation and merging
  • Integration tests for sync and async tables
  • Transactional delete validation
  • Version match success scenarios
  • Version mismatch failure scenarios
  • Backward compatibility verification

All new and existing tests pass successfully.

Test Coverage on modified classes:

image

Test Coverage Checklist

Scenario Done Comments if Not Done
1. Different TableSchema Creation Methods
a. TableSchema.fromBean(Customer.class) [x]
b. TableSchema.fromImmutableClass(Customer.class) for immutable classes [x]
c. TableSchema.documentSchemaBuilder().build() [ ]
d. StaticTableSchema.builder(Customer.class) [x]
2. Nesting of Different TableSchema Types
a. @DynamoDbBean with annotated auto-generated key []
b. @DynamoDbImmutable with annotated auto-generated key []
c. Auto-generated key combined with partition/sort key []
3. CRUD Operations
a. scan() [ ]
b. query() [x]
c. updateItem() preserves existing value or generates when absent [x]
d. putItem() with no key set (auto-generation occurs) [x]
e. putItem() with key set manually [x]
f. getItem() retrieves auto-generated key [x]
g. deleteItem() [x]
h. batchGetItem() [ ]
i. batchWriteItem() [ ]
j. transactGetItems() [ ]
k. transactWriteItems() [x]
4. Data Types and Null Handling
a. Annotated attribute is null → key auto-generated []
b. Annotated attribute non-null → value preserved []
c. Validation rejects non-String annotated attribute []
5. AsyncTable and SyncTable
a. DynamoDbAsyncTable Testing [x]
b. DynamoDbTable Testing [x]
6. New/Modification in Extensions
a. Works with other extensions like VersionedRecordExtension [x]
b. Test with Default Values in Annotations [ ]
c. Combination of Annotation and Builder passes extension [ ]
7. New/Modification in Converters
a. Tables with Scenario in ScenarioSl No.1 (All table schemas are Must) [ ]
b. Test with Default Values in Annotations [ ]
c. Test All Scenarios from 1 to 5 [ ]

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the CONTRIBUTING document
  • Local run of mvn install succeeds
  • My code follows the code style of this project
  • My change requires a change to the Javadoc documentation
  • I have updated the Javadoc documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed
  • I have added a changelog entry. Adding a new entry must be accomplished by running the scripts/new-change script and following the instructions. Commit the new file created by the script in .changes/next-release with your changes.
  • My change is to implement 1.11 parity feature and I have updated LaunchChangelog

License

  • I confirm that this pull request can be released under the Apache 2 license

@anasatirbasa anasatirbasa marked this pull request as ready for review February 23, 2026 15:13
@anasatirbasa anasatirbasa requested a review from a team as a code owner February 23, 2026 15:13
*/
public static Expression createVersionCondition(AttributeValue versionValue, String versionAttributeName) {
return Expression.builder()
.expression(versionAttributeName + " = :version_value")

Choose a reason for hiding this comment

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

This uses the raw attribute name directly in the expression string. The existing VersionedRecordExtension uses keyRef() and expressionNames maps to safely handle DynamoDB reserved words. Should we do the same thing here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have updated the code in order to use keyRef(), in the same way it is used in VersionedRecordExtension.

* @throws IllegalArgumentException if any parameter is null
*/
public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) {
Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName);

Choose a reason for hiding this comment

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

is it possible that withOptimisticLocking() silently overwrites any existing conditionExpression ?

should this use Use Expression.join() to merge with any existing condition instead ?

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.

I have updated the code in order to merge the optimistic locking condition with the other existing conditions, if available:

 /**
         * Adds optimistic locking to this delete request.
         * <p>
         * 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 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) {
            Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName);
            this.conditionExpression = Expression.join(this.conditionExpression, optimisticLockingCondition, " AND ");
            return this;
        }

* @throws IllegalArgumentException if any parameter is null
*/
public Builder withOptimisticLocking(AttributeValue versionValue, String versionAttributeName) {
Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName);

Choose a reason for hiding this comment

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

is it possible that withOptimisticLocking() silently overwrites any existing conditionExpression ?

should this use Use Expression.join() to merge with any existing condition instead ?

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.

I have updated the code in order to merge the optimistic locking condition with the other existing conditions, if available:

 /**
         * Adds optimistic locking to this delete request.
         * <p>
         * 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 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) {
            Expression optimisticLockingCondition = createVersionCondition(versionValue, versionAttributeName);
            this.conditionExpression = Expression.join(this.conditionExpression, optimisticLockingCondition, " AND ");
            return this;
        }

@Deprecated
public T deleteItem(T keyItem) {
return deleteItem(keyFrom(keyItem));
return deleteItem(keyItem, false);

Choose a reason for hiding this comment

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

This is more of a question, the old deleteItem(T keyItem) used to call deleteItem(keyFrom(keyItem)) which goes through deleteItem(Key) → deleteItem(Consumer) → deleteItem(DeleteItemEnhancedRequest). The new code delegates to deleteItem(keyItem, false) which builds a DeleteItemEnhancedRequest directly and calls deleteItem(DeleteItemEnhancedRequest). It is called with the false flag means no optimistic locking is applied, the code path is different. The old path went through the Consumer overload, the new path constructs the request directly. If any subclass overrides the Consumer-based overload (which is a default method on the interface, so unlikely but possible), this changes behavior.

Is this a concern ? how do we think about backward compatibility ? (same applies to async table)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are right — this could introduce a potential backward compatibility issue.

Previously, deleteItem(T keyItem) delegated through the Consumer-based overload.
The new implementation calls deleteItem(keyItem, false) directly, which bypasses that path.

Although it is unlikely that users override the Consumer-based method (since it is a default interface method), it is technically possible. In such a case, behavior could change.

To avoid breaking existing behavior, I kept the old method unchanged and marked it as deprecated:

/**
 * @deprecated Use {@link #deleteItem(Object, boolean)} instead to explicitly
 * control optimistic locking behavior.
 */

This preserves backward compatibility while guiding users toward the new API.
The same approach was applied to the async table implementation.

// 2. deleteItem(T item) on Versioned record and versions match
// -> Optimistic Locking is applied -> deletes the record
@Test
public void deleteItem_onVersionedRecord_whenVersionsMatch_appliesOptimisticLockingAndDeletesTheRecord() {

Choose a reason for hiding this comment

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

The test suggests optimistic locking is applied. But deleteItem(savedItem) is the deprecated overload, which calls deleteItem(keyItem, false) optimistic locking is off. The delete goes to DynamoDB as an unconditional DeleteItem with no condition expression. No version check happens I guess ? (same applies to async version test)

@Test
public void deleteItem_onVersionedRecord_whenVersionsMatch_appliesOptimisticLockingAndDeletesTheRecord() {
    VersionedRecord item = new VersionedRecord().setId("123").setSort(10).setStringAttribute("Test Item");
    versionedRecordTable.putItem(item);                                    // version becomes 1
    VersionedRecord savedItem = versionedRecordTable.getItem(r -> r.key(recordKey));  // gets item with version=1
    versionedRecordTable.deleteItem(savedItem);                            // calls deprecated overload
    // asserts item is deleted
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have modified the tests and added 3 new scenarios for the deprecated method:

1. Non-versioned record:
When deleting a non-versioned record using the deprecated method, optimistic locking is not applied
and the record is deleted unconditionally.

@Test
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);
    Record savedItem = mappedTable.getItem(r -> r.key(recordKey));
    mappedTable.deleteItem(savedItem);

    Record deletedItem = mappedTable.getItem(r -> r.key(recordKey));
    assertThat(deletedItem).isNull();
}

2. Versioned record with matching version:
When deleting a versioned record using the deprecated method, optimistic locking is not enforced,
even when the version matches. The record is deleted unconditionally.

@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. Versioned record with stale version:
When deleting a versioned record with a stale version using the deprecated method, optimistic locking is still not applied. The record is deleted even though the version does not match.

@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);
    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 deprecated method does not apply optimistic locking.
    assertThat(deletedItem).isNull();
}

}

@Test
public void buildDeleteItemEnhancedRequest_preservesExistingRequestProperties() {

Choose a reason for hiding this comment

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

you may want to also create another test around here, like builder_withOptimisticLocking_afterConditionExpression_shouldMergeBothConditions to verify the behavior

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added a test in that checks this:

@Test  public void buildDeleteItemEnhancedRequest_withOptimisticLockingAndCustomCondition_mergesConditions() {
        Key key = Key.builder().partitionValue("id").build();
        AttributeValue versionValue = AttributeValue.builder().n("1").build();
        String versionAttributeName = "version";

        Map<String, String> expressionNames = new HashMap<>();
        expressionNames.put("#key1", "key1");
        expressionNames.put("#key2", "key2");

        Map<String, AttributeValue> 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 = :AMZN_MAPPED_version)");

        Map<String, String> expectedExpressionNames = new HashMap<>();
        expectedExpressionNames.put("#AMZN_MAPPED_version", "version");
        expectedExpressionNames.put("#key1", "key1");
        expectedExpressionNames.put("#key2", "key2");
        assertThat(result.conditionExpression().expressionNames()).containsExactlyInAnyOrderEntriesOf(expectedExpressionNames);

        Map<String, AttributeValue> expectedExpressionValues = new HashMap<>();
        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);
}

import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;

public class OptimisticLockingCrudTest extends LocalDynamoDbSyncTestBase {

Choose a reason for hiding this comment

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

Do we want to have a test that calls the deprecated deleteItem(savedItem) with a stale version and verify it still deletes (because no condition is applied (probably also in OptimisticLockingCrudTest)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added a test for this scenario in both OptimisticLockingCrudTest / OptimisticLockingAsyncCrudTest:

// 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);
        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();
    }

import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;

public class OptimisticLockingCrudTest extends LocalDynamoDbSyncTestBase {

Choose a reason for hiding this comment

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

deleteItem(Consumer) path with optimistic locking via builder -> The deleteItem(Consumer) overload is never tested with optimistic locking. (also in OptimisticLockingCrudTest)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added tests for The deleteItem(Consumer) overload for both sync / async implementations.

For each of them, 6 scenarios were defined:

  1. deleteItemWithConsumer_onVersionedRecord_whenVersionsMatch_deletesTheRecord
    Versioned record with optimistic locking enabled and matching version. The operation fails and the record is not deleted.

  2. deleteItemWithConsumer_onVersionedRecord_whenVersionsMismatch_doesNotDeleteTheRecord
    Versioned record with optimistic locking enabled but mismatching version. The operation fails and the record is not deleted.

  3. deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionAreRespected_deletesTheRecord
    Version matches and the custom condition expression evaluates successfully. The record is deleted.

  4. deleteItemWithConsumer_whenOptimisticLockingConditionRespected_butCustomConditionFails_doesNotDeleteTheRecord
    Version matches but the custom condition expression fails. The operation fails and the record is not deleted.

  5. deleteItemWithConsumer_whenCustomConditionRespected_butOptimisticConditionFails_doesNotDeleteTheRecord
    Custom condition passes but the optimistic locking version check fails. The operation fails and the record is not deleted.

  6. deleteItemWithConsumer_whenOptimisticLockingAndCustomConditionNotRespected_doesNotDeleteTheRecord
    Both the optimistic locking check and the custom conditions expression fail. The operation fails and the record is not deleted.

import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;

Choose a reason for hiding this comment

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

can test deleteItem on an item that doesn't exist in the table, no test covers calling deleteItem(item, true) when the item doesn't exist in DynamoDB. With optimistic locking enabled and a non-null version on the item, the condition version = :version_value will fail because the item doesn't exist.

}

@Test
public void conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersionedRecord_addsConditionExpression() {

Choose a reason for hiding this comment

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

the test name says "adds condition expression" but the assertion proves the opposite

  1. optimisticLockingEnabled = true
  2. Uses RecordWithUpdateBehaviors — this IS a versioned record (has @DynamoDbVersionAttribute)
  3. But keyItem.setVersion(...) is never called — the version field is null
  4. conditionallyApplyOptimisticLocking finds the version attribute name in the schema, calls tableSchema.attributeValue(keyItem, versionAttributeName), gets null, and hits the version != null ? ... : request branch — returning the original request unchanged
    5.The assertion is assertEquals(originalRequest, result) — confirming no condition was added

if this is the purpose, should the test name be something like conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersionedRecordWithNullVersion_returnsOriginalRequest ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the observation!

I have updated the test name in order to reflect the correct behavior: conditionallyApplyOptimistic_onTransactDelete_whenFlagTrueAndVersionedRecordWithNullVersion_returnsOriginalRequest.

DeleteItemEnhancedRequest request =
DeleteItemEnhancedRequest.builder()
.key(Key.builder().partitionValue("id").build())
.withOptimisticLocking(versionValue, "version")

Choose a reason for hiding this comment

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

.with is more of an SDKv1 approach, AFAIK, SDKv2 does not use this naming convention?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the builder method name to "optimisticLocking".

* @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".

@Override
public CompletableFuture<T> deleteItem(T keyItem, boolean useOptimisticLocking) {
DeleteItemEnhancedRequest request = DeleteItemEnhancedRequest.builder().key(keyFrom(keyItem)).build();
request = conditionallyApplyOptimisticLocking(request, keyItem, tableSchema, useOptimisticLocking);

Choose a reason for hiding this comment

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

I think we should pass the builder here as the conditionallyApplyOptimisticLocking may need to add (overwrite actually) the condition expression with the OptLocking condition. Builder objects work well in these cases where the building logic is complex and scattered.

Here the withOptimisticLocking methods in OptimisticLockingHelper won't need to rebuild the request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have updated the method in order to pass the builder instead of the actual request.

Comment on lines 142 to 143
.expression(String.format("%s = :version_value", attributeKeyRef))
.putExpressionValue(":version_value", versionValue)

Choose a reason for hiding this comment

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

I think we should use keyRef and valueRef to prevent collisions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have done the changes in order to use "keyRef" and "valueRef":

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();

* @throws IllegalArgumentException if {@code versionAttributeName} is 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.");
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants