1717
1818import static org .assertj .core .api .Assertions .assertThat ;
1919import static org .assertj .core .api .Assertions .assertThatThrownBy ;
20+ import static org .junit .Assert .assertEquals ;
21+ import static org .junit .Assert .assertThrows ;
22+ import static org .junit .Assert .assertTrue ;
2023
2124import org .assertj .core .data .Offset ;
2225import org .junit .After ;
2326import org .junit .AfterClass ;
2427import org .junit .BeforeClass ;
2528import org .junit .Test ;
29+ import software .amazon .awssdk .enhanced .dynamodb .extensions .VersionedRecordExtension ;
2630import software .amazon .awssdk .enhanced .dynamodb .model .DeleteItemEnhancedRequest ;
2731import software .amazon .awssdk .enhanced .dynamodb .model .DeleteItemEnhancedResponse ;
2832import software .amazon .awssdk .enhanced .dynamodb .model .EnhancedLocalSecondaryIndex ;
2933import software .amazon .awssdk .enhanced .dynamodb .model .GetItemEnhancedResponse ;
3034import software .amazon .awssdk .enhanced .dynamodb .model .PutItemEnhancedRequest ;
3135import software .amazon .awssdk .enhanced .dynamodb .model .PutItemEnhancedResponse ;
3236import software .amazon .awssdk .enhanced .dynamodb .model .Record ;
37+ import software .amazon .awssdk .enhanced .dynamodb .model .TransactDeleteItemEnhancedRequest ;
38+ import software .amazon .awssdk .enhanced .dynamodb .model .TransactWriteItemsEnhancedRequest ;
3339import software .amazon .awssdk .enhanced .dynamodb .model .UpdateItemEnhancedRequest ;
3440import software .amazon .awssdk .enhanced .dynamodb .model .UpdateItemEnhancedResponse ;
41+ import software .amazon .awssdk .enhanced .dynamodb .model .VersionedRecord ;
3542import software .amazon .awssdk .services .dynamodb .DynamoDbClient ;
43+ import software .amazon .awssdk .services .dynamodb .model .AttributeValue ;
3644import software .amazon .awssdk .services .dynamodb .model .ConditionalCheckFailedException ;
3745import software .amazon .awssdk .services .dynamodb .model .ConsumedCapacity ;
3846import software .amazon .awssdk .services .dynamodb .model .Projection ;
3947import software .amazon .awssdk .services .dynamodb .model .ProjectionType ;
4048import software .amazon .awssdk .services .dynamodb .model .ReturnConsumedCapacity ;
4149import software .amazon .awssdk .services .dynamodb .model .ReturnItemCollectionMetrics ;
4250import software .amazon .awssdk .services .dynamodb .model .ReturnValuesOnConditionCheckFailure ;
51+ import software .amazon .awssdk .services .dynamodb .model .TransactionCanceledException ;
4352
4453public class CrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegrationTestBase {
4554
4655 private static final String TABLE_NAME = createTestTableName ();
56+ private static final String VERSIONED_TABLE_NAME = createTestTableName ();
4757
4858 private static final EnhancedLocalSecondaryIndex LOCAL_SECONDARY_INDEX =
4959 EnhancedLocalSecondaryIndex .builder ()
@@ -56,27 +66,39 @@ public class CrudWithResponseIntegrationTest extends DynamoDbEnhancedIntegration
5666 private static DynamoDbClient dynamoDbClient ;
5767 private static DynamoDbEnhancedClient enhancedClient ;
5868 private static DynamoDbTable <Record > mappedTable ;
69+ private static DynamoDbTable <VersionedRecord > versionedRecordTable ;
5970
6071 @ BeforeClass
6172 public static void beforeClass () {
6273 dynamoDbClient = createDynamoDbClient ();
63- enhancedClient = DynamoDbEnhancedClient .builder ().dynamoDbClient (dynamoDbClient ).build ();
74+ enhancedClient = DynamoDbEnhancedClient .builder ()
75+ .dynamoDbClient (dynamoDbClient )
76+ .extensions (VersionedRecordExtension .builder ().build ())
77+ .build ();
6478 mappedTable = enhancedClient .table (TABLE_NAME , TABLE_SCHEMA );
6579 mappedTable .createTable (r -> r .localSecondaryIndices (LOCAL_SECONDARY_INDEX ));
80+ versionedRecordTable = enhancedClient .table (VERSIONED_TABLE_NAME , VERSIONED_RECORD_TABLE_SCHEMA );
81+ versionedRecordTable .createTable ();
6682 dynamoDbClient .waiter ().waitUntilTableExists (r -> r .tableName (TABLE_NAME ));
83+ dynamoDbClient .waiter ().waitUntilTableExists (r -> r .tableName (VERSIONED_TABLE_NAME ));
6784 }
6885
6986 @ After
7087 public void tearDown () {
7188 mappedTable .scan ()
7289 .items ()
7390 .forEach (record -> mappedTable .deleteItem (record ));
91+
92+ versionedRecordTable .scan ()
93+ .items ()
94+ .forEach (versionedRecord -> versionedRecordTable .deleteItem (versionedRecord ));
7495 }
7596
7697 @ AfterClass
7798 public static void afterClass () {
7899 try {
79100 dynamoDbClient .deleteTable (r -> r .tableName (TABLE_NAME ));
101+ dynamoDbClient .deleteTable (r -> r .tableName (VERSIONED_TABLE_NAME ));
80102 } finally {
81103 dynamoDbClient .close ();
82104 }
@@ -321,4 +343,213 @@ public void getItem_set_stronglyConsistent() {
321343 // A strongly consistent read request of an item up to 4 KB requires one read request unit.
322344 assertThat (consumedCapacity .capacityUnits ()).isCloseTo (20.0 , Offset .offset (1.0 ));
323345 }
324- }
346+
347+ // ========== OPTIMISTIC LOCKING TESTS ==========
348+
349+ // 1. deleteItem(T item) - Non-versioned record
350+ @ Test
351+ public void deleteItem_nonVersionedRecord_shouldSucceed () {
352+ Record item = new Record ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
353+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
354+
355+ mappedTable .putItem (item );
356+ mappedTable .deleteItem (item );
357+
358+ Record deletedItem = mappedTable .getItem (r -> r .key (recordKey ));
359+ assertThat (deletedItem ).isNull ();
360+ }
361+
362+ // 2. deleteItem(T item) - Versioned record, versions match
363+ @ Test
364+ public void deleteItem_versionedRecord_versionMatch_shouldSucceed () {
365+ VersionedRecord item = new VersionedRecord ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
366+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
367+
368+ versionedRecordTable .putItem (item );
369+ VersionedRecord savedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
370+ versionedRecordTable .deleteItem (savedItem );
371+
372+ VersionedRecord deletedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
373+ assertThat (deletedItem ).isNull ();
374+ }
375+
376+ // 3. deleteItem(T item, false) - Versioned record, should not use optimistic locking
377+ @ Test
378+ public void deleteItem_versionedRecord_flagFalse_shouldSucceed () {
379+ VersionedRecord item = new VersionedRecord ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
380+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
381+
382+ versionedRecordTable .putItem (item );
383+ VersionedRecord savedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
384+
385+ // Update the item to change its version
386+ savedItem .setStringAttribute ("Updated Item" );
387+ versionedRecordTable .updateItem (savedItem );
388+
389+ // Delete with old version but flag=false - should succeed (no optimistic locking)
390+ VersionedRecord oldVersionItem = new VersionedRecord ().setId ("123" ).setSort (10 ).setVersion (1 );
391+ versionedRecordTable .deleteItem (oldVersionItem , false );
392+
393+ VersionedRecord deletedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
394+ assertThat (deletedItem ).isNull ();
395+ }
396+
397+ // 4. deleteItem(T item, true) - Versioned record, versions match
398+ @ Test
399+ public void deleteItem_versionedRecord_flagTrue_versionMatch_shouldSucceed () {
400+ VersionedRecord item = new VersionedRecord ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
401+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
402+
403+ versionedRecordTable .putItem (item );
404+ VersionedRecord savedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
405+ versionedRecordTable .deleteItem (savedItem , true );
406+
407+ VersionedRecord deletedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
408+ assertThat (deletedItem ).isNull ();
409+ }
410+
411+ // 5. deleteItem(T item, true) - Versioned record, versions mismatch
412+ @ Test
413+ public void deleteItem_versionedRecord_flagTrue_versionMismatch_shouldFail () {
414+ VersionedRecord item = new VersionedRecord ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
415+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
416+
417+ versionedRecordTable .putItem (item );
418+ VersionedRecord savedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
419+
420+ // Update the item to change its version
421+ savedItem .setStringAttribute ("Updated Item" );
422+ versionedRecordTable .updateItem (savedItem );
423+
424+ // Try to delete with old version and flag=true - should fail
425+ VersionedRecord oldVersionItem = new VersionedRecord ().setId ("123" ).setSort (10 ).setVersion (1 );
426+
427+ assertThatThrownBy (() -> versionedRecordTable .deleteItem (oldVersionItem , true ))
428+ .isInstanceOf (ConditionalCheckFailedException .class )
429+ .satisfies (e -> assertThat (e .getMessage ()).contains ("The conditional request failed" ));
430+ }
431+
432+
433+
434+
435+ // 6. deleteItem(DeleteItemEnhancedRequest) with builder method - versions match
436+ @ Test
437+ public void deleteItemWithBuilder_versionMatch_shouldSucceed () {
438+ VersionedRecord item = new VersionedRecord ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
439+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
440+
441+ versionedRecordTable .putItem (item );
442+ VersionedRecord savedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
443+
444+ DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest .builder ()
445+ .key (recordKey )
446+ .withOptimisticLocking (AttributeValue .builder ().n (savedItem .getVersion ().toString ()).build (), "version" )
447+ .build ();
448+
449+ versionedRecordTable .deleteItem (requestWithLocking );
450+
451+ VersionedRecord deletedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
452+ assertThat (deletedItem ).isNull ();
453+ }
454+
455+ // 7. deleteItem(DeleteItemEnhancedRequest) with builder method - versions mismatch
456+ @ Test
457+ public void deleteItemWithBuilder_versionMismatch_shouldFail () {
458+ VersionedRecord item = new VersionedRecord ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
459+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
460+
461+ versionedRecordTable .putItem (item );
462+
463+ DeleteItemEnhancedRequest requestWithLocking = DeleteItemEnhancedRequest .builder ()
464+ .key (recordKey )
465+ .withOptimisticLocking (AttributeValue .builder ().n ("999" ).build (), "version" )
466+ .build ();
467+
468+ assertThatThrownBy (() -> versionedRecordTable .deleteItem (requestWithLocking ))
469+ .isInstanceOf (ConditionalCheckFailedException .class )
470+ .satisfies (e -> assertThat (e .getMessage ()).contains ("The conditional request failed" ));
471+ }
472+
473+ // 8. TransactWriteItems.addDeleteItem(T item) - Non-versioned record
474+ @ Test
475+ public void transactDeleteItem_nonVersionedRecord_shouldSucceed () {
476+ Record item = new Record ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
477+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
478+
479+ mappedTable .putItem (item );
480+
481+ enhancedClient .transactWriteItems (TransactWriteItemsEnhancedRequest .builder ()
482+ .addDeleteItem (mappedTable , item )
483+ .build ());
484+
485+ Record deletedItem = mappedTable .getItem (r -> r .key (recordKey ));
486+ assertThat (deletedItem ).isNull ();
487+ }
488+
489+ // 9. TransactWriteItems.addDeleteItem(T item) - Versioned record, versions match
490+ @ Test
491+ public void transactDeleteItem_versionedRecord_versionMatch_shouldSucceed () {
492+ VersionedRecord item = new VersionedRecord ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
493+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
494+
495+ versionedRecordTable .putItem (item );
496+ VersionedRecord savedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
497+
498+ enhancedClient .transactWriteItems (TransactWriteItemsEnhancedRequest .builder ()
499+ .addDeleteItem (versionedRecordTable , savedItem )
500+ .build ());
501+
502+ VersionedRecord deletedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
503+ assertThat (deletedItem ).isNull ();
504+ }
505+
506+
507+
508+
509+ // 10. TransactWriteItems with builder method - versions match
510+ @ Test
511+ public void transactDeleteItemWithBuilder_versionMatch_shouldSucceed () {
512+ VersionedRecord item = new VersionedRecord ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
513+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
514+
515+ versionedRecordTable .putItem (item );
516+ VersionedRecord savedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
517+
518+ TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest .builder ()
519+ .key (recordKey )
520+ .withOptimisticLocking (AttributeValue .builder ().n (savedItem .getVersion ().toString ()).build (), "version" )
521+ .build ();
522+
523+ enhancedClient .transactWriteItems (TransactWriteItemsEnhancedRequest .builder ()
524+ .addDeleteItem (versionedRecordTable ,
525+ requestWithLocking )
526+ .build ());
527+
528+ VersionedRecord deletedItem = versionedRecordTable .getItem (r -> r .key (recordKey ));
529+ assertThat (deletedItem ).isNull ();
530+ }
531+
532+ // 11. TransactWriteItems with builder method - versions mismatch
533+ @ Test
534+ public void transactDeleteItemWithBuilder_versionMismatch_shouldFail () {
535+ VersionedRecord item = new VersionedRecord ().setId ("123" ).setSort (10 ).setStringAttribute ("Test Item" );
536+ Key recordKey = Key .builder ().partitionValue (item .getId ()).sortValue (item .getSort ()).build ();
537+
538+ versionedRecordTable .putItem (item );
539+
540+ TransactDeleteItemEnhancedRequest requestWithLocking = TransactDeleteItemEnhancedRequest .builder ()
541+ .key (recordKey )
542+ .withOptimisticLocking (AttributeValue .builder ().n ("999" ).build (), "version" )
543+ .build ();
544+
545+ TransactionCanceledException ex = assertThrows (TransactionCanceledException .class ,
546+ () -> enhancedClient .transactWriteItems (TransactWriteItemsEnhancedRequest .builder ()
547+ .addDeleteItem (versionedRecordTable , requestWithLocking )
548+ .build ()));
549+
550+ assertTrue (ex .hasCancellationReasons ());
551+ assertEquals (1 , ex .cancellationReasons ().size ());
552+ assertEquals ("ConditionalCheckFailed" , ex .cancellationReasons ().get (0 ).code ());
553+ assertEquals ("The conditional request failed" , ex .cancellationReasons ().get (0 ).message ());
554+ }
555+ }
0 commit comments