customIncrementForExistingVersionValues() {
Arguments.of(3L, null, 10L, "11"),
Arguments.of(null, 3L, 4L, "7"));
}
-
- @Test
- public void versionAttribute_withInvalidStartAt_throwsIllegalArgumentException() {
- assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() ->
- StaticTableSchema.builder(TestItem.class)
- .newItemSupplier(TestItem::new)
- .addAttribute(String.class,
- a -> a.name("id")
- .getter(TestItem::getId)
- .setter(TestItem::setId)
- .addTag(primaryPartitionKey()))
- .addAttribute(Long.class,
- a -> a.name("version")
- .getter(TestItem::getVersion)
- .setter(TestItem::setVersion)
- .addTag(versionAttribute(-2L, 1L)))
- .build()
- )
- .withMessage("startAt must be -1 or greater.");
- }
-
- @Test
- public void versionAttribute_withInvalidIncrementBy_throwsIllegalArgumentException() {
- assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() ->
- StaticTableSchema.builder(TestItem.class)
- .newItemSupplier(TestItem::new)
- .addAttribute(String.class,
- a -> a.name("id")
- .getter(TestItem::getId)
- .setter(TestItem::setId)
- .addTag(primaryPartitionKey()))
- .addAttribute(Long.class,
- a -> a.name("version")
- .getter(TestItem::getVersion)
- .setter(TestItem::setVersion)
- .addTag(versionAttribute(0L, 0L)))
- .build()
- )
- .withMessage("incrementBy must be greater than 0.");
- }
-
- @Test
- public void versionAttribute_withNonNumericType_throwsIllegalArgumentException() {
- assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() ->
- StaticTableSchema.builder(TestItem.class)
- .newItemSupplier(TestItem::new)
- .addAttribute(String.class,
- a -> a.name("id")
- .getter(TestItem::getId)
- .setter(TestItem::setId)
- .addTag(primaryPartitionKey()))
- .addAttribute(String.class,
- a -> a.name("version")
- .getter(TestItem::getId)
- .setter(TestItem::setId)
- .addTag(versionAttribute()))
- .build()
- )
- .withMessageContaining(
- "is not a suitable type to be used as a version attribute. Only type 'N' is supported.");
- }
-
- private static class TestItem {
- private String id;
- private Long version;
-
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public Long getVersion() {
- return version;
- }
-
- public void setVersion(Long version) {
- this.version = version;
- }
- }
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
index 506789cadd0c..ff811115937b 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
@@ -48,9 +48,12 @@
/**
* Tests for @DynamoDbAutoGeneratedKey annotation functionality.
*
- * Tests cover: - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK) - UpdateBehavior control (WRITE_ALWAYS vs
- * WRITE_IF_NOT_EXISTS) for secondary index keys - Primary key limitations (UpdateBehavior has no effect) - Error handling for
- * invalid usage - Integration with other extensions (VersionedRecord)
+ * Tests cover:
+ * - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK)
+ * - UpdateBehavior control (WRITE_ALWAYS vs WRITE_IF_NOT_EXISTS) for secondary index keys
+ * - Primary key limitations (UpdateBehavior has no effect)
+ * - Error handling for invalid usage
+ * - Integration with other extensions (VersionedRecord)
*/
@RunWith(Parameterized.class)
public class AutoGeneratedKeyRecordTest extends LocalDynamoDbSyncTestBase {
@@ -116,7 +119,6 @@ public void deleteTable() {
@Test
public void putItem_generatesUuidsForAllFourKeyTypes() {
TestRecord record = new TestRecord();
- // Don't set any keys - they should all be auto-generated
mappedTable.putItem(record);
TestRecord retrieved = mappedTable.scan().items().stream().findFirst()
@@ -128,7 +130,7 @@ public void putItem_generatesUuidsForAllFourKeyTypes() {
assertValidUuid(retrieved.getGsiPk()); // GSI partition key
assertValidUuid(retrieved.getGsiSk()); // GSI sort key
- // Verify they're all different
+ // Verify they're all unique keys
assertThat(retrieved.getId()).isNotEqualTo(retrieved.getSortKey());
assertThat(retrieved.getGsiPk()).isNotEqualTo(retrieved.getGsiSk());
assertThat(retrieved.getId()).isNotEqualTo(retrieved.getGsiPk());
@@ -136,7 +138,7 @@ public void putItem_generatesUuidsForAllFourKeyTypes() {
@Test
public void updateItem_respectsUpdateBehaviorForSecondaryIndexKeys() {
- // Put initial record
+ // Put record
TestRecord record = new TestRecord();
mappedTable.putItem(record);
@@ -145,7 +147,7 @@ public void updateItem_respectsUpdateBehaviorForSecondaryIndexKeys() {
String id = afterPut.getId();
String sortKey = afterPut.getSortKey();
String originalGsiPk = afterPut.getGsiPk(); // WRITE_ALWAYS (default) → should change
- String originalGsiSk = afterPut.getGsiSk(); // WRITE_IF_NOT_EXISTS → should preserve
+ String originalGsiSk = afterPut.getGsiSk(); // WRITE_IF_NOT_EXISTS → should preserve
// Update record
TestRecord updateRecord = new TestRecord();
@@ -161,7 +163,7 @@ public void updateItem_respectsUpdateBehaviorForSecondaryIndexKeys() {
}
@Test
- public void nonKeyAttribute_throwsException() {
+ public void putItem_withAutogeneratedKeySetOnNonKeyAttribute_throwsException() {
String tableName = getConcreteTableName("invalid-usage-test");
DynamoDbEnhancedClient client = createClient();
@@ -231,7 +233,7 @@ public void versionedRecord_worksWithAutoGeneratedKeys() {
}
@Test
- public void conflictingAnnotations_throwsException() {
+ public void autogeneratedConflictingAnnotations_throwsException() {
String tableName = getConcreteTableName("conflicting-annotations-test");
DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder()
.dynamoDbClient(getDynamoDbClient())
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java
index 3bdc7a5eb218..d513df433c35 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java
@@ -312,8 +312,8 @@ public void updateItemConditionTestFailure() {
}
@Test
- public void conflictingAnnotations_throwsException() {
- String tableName = getConcreteTableName("conflicting-annotations-test");
+ public void putItem_onRecordWithAutogeneratedConflictingAnnotations_throwsException() {
+ String tableName = getConcreteTableName("conflicting-annotations-record-table");
DynamoDbEnhancedClient client =
DynamoDbEnhancedClient.builder()
.dynamoDbClient(getDynamoDbClient())
@@ -326,7 +326,7 @@ public void conflictingAnnotations_throwsException() {
table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
ConflictingAnnotationsRecord record = new ConflictingAnnotationsRecord();
- record.setPayload("test");
+ record.setPayload("payload");
Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> table.putItem(record))
@@ -508,8 +508,8 @@ public static class ConflictingAnnotationsRecord {
// Both annotations on the same field - should cause an exception
@DynamoDbPartitionKey
- @DynamoDbAutoGeneratedUuid
@DynamoDbAutoGeneratedKey
+ @DynamoDbAutoGeneratedUuid
public String getId() {
return id;
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
index 52f80818216e..6e24fe2d96e0 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
@@ -78,6 +78,7 @@ public void putItem_whenKeysNotPopulated_generatesNewUuids() {
mappedTable.putItem(record);
AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst()
.orElseThrow(() -> new AssertionError("No record found"));
+
isValidUuid(result.getId());
isValidUuid(result.getSortKey());
isValidUuid(result.getGsiPk());
@@ -137,15 +138,15 @@ public void updateItem_respectsUpdateBehavior() {
}
@Test
- public void batchWrite_whenKeysNotPopulated_generatesNewUuids() {
- AutogeneratedKeyRecord record1 = new AutogeneratedKeyRecord();
- AutogeneratedKeyRecord record2 = new AutogeneratedKeyRecord();
+ public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
+ AutogeneratedKeyRecord firstRecord = new AutogeneratedKeyRecord();
+ AutogeneratedKeyRecord secondRecord = new AutogeneratedKeyRecord();
enhancedClient.batchWriteItem(req -> req.addWriteBatch(
WriteBatch.builder(AutogeneratedKeyRecord.class)
.mappedTableResource(mappedTable)
- .addPutItem(record1)
- .addPutItem(record2)
+ .addPutItem(firstRecord)
+ .addPutItem(secondRecord)
.build()));
List results = mappedTable.scan().items().stream().collect(Collectors.toList());
@@ -158,23 +159,23 @@ public void batchWrite_whenKeysNotPopulated_generatesNewUuids() {
@Test
public void batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids() {
- AutogeneratedKeyRecord record1 = new AutogeneratedKeyRecord();
- record1.setId("existing-id-1");
- record1.setSortKey("existing-sk-1");
- record1.setGsiPk("existing-gsiPk-1");
- record1.setGsiSk("existing-gsiSk-1");
-
- AutogeneratedKeyRecord record2 = new AutogeneratedKeyRecord();
- record2.setId("existing-id-2");
- record2.setSortKey("existing-sk-2");
- record2.setGsiPk("existing-gsiPk-2");
- record2.setGsiSk("existing-gsiSk-2");
+ AutogeneratedKeyRecord firstRecord = new AutogeneratedKeyRecord();
+ firstRecord.setId("existing-id-1");
+ firstRecord.setSortKey("existing-sk-1");
+ firstRecord.setGsiPk("existing-gsiPk-1");
+ firstRecord.setGsiSk("existing-gsiSk-1");
+
+ AutogeneratedKeyRecord secondRecord = new AutogeneratedKeyRecord();
+ secondRecord.setId("existing-id-2");
+ secondRecord.setSortKey("existing-sk-2");
+ secondRecord.setGsiPk("existing-gsiPk-2");
+ secondRecord.setGsiSk("existing-gsiSk-2");
enhancedClient.batchWriteItem(req -> req.addWriteBatch(
WriteBatch.builder(AutogeneratedKeyRecord.class)
.mappedTableResource(mappedTable)
- .addPutItem(record1)
- .addPutItem(record2)
+ .addPutItem(firstRecord)
+ .addPutItem(secondRecord)
.build()));
AutogeneratedKeyRecord savedRecord1 =
@@ -193,7 +194,7 @@ public void batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids() {
}
@Test
- public void transactWrite_whenKeysNotPopulated_generatesNewUuids() {
+ public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
AutogeneratedKeyRecord record = new AutogeneratedKeyRecord();
enhancedClient.transactWriteItems(
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java
index 03588232bd2b..c9a29f0e4563 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java
@@ -70,13 +70,14 @@ public void deleteTable() {
}
@Test
- public void putItem_whenKeysNotPopulated_generatesNewUuids() {
+ public void putItem_whenKeysNotAlreadyPopulated_generatesNewUuids() {
RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid();
record.setId("existing-id");
mappedTable.putItem(record);
RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst()
.orElseThrow(() -> new AssertionError("No record found"));
+
isValidUuid(result.getId());
isValidUuid(result.getSortKey());
}
@@ -98,7 +99,7 @@ public void putItem_whenKeysAlreadyPopulated_replacesExistingUuids() {
}
@Test
- public void batchWrite_whenKeysNotPopulated_generatesNewUuids() {
+ public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
RecordWithAutogeneratedUuid record1 = new RecordWithAutogeneratedUuid();
RecordWithAutogeneratedUuid record2 = new RecordWithAutogeneratedUuid();
@@ -149,7 +150,7 @@ public void batchWrite_whenKeysAlreadyPopulated_generatesNewUuids() {
}
@Test
- public void transactWrite_whenKeysNotPopulated_generatesNewUuids() {
+ public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid();
enhancedClient.transactWriteItems(
@@ -183,7 +184,7 @@ public void transactWrite_whenKeysAlreadyPopulated_generatesNewUuids() {
}
@Test
- public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_doesNotRegenerateUuids() {
+ public void putItem_whenAutogeneratedUuidAnnotationIsNotPresent_doesNotRegenerateUuids() {
String tableName = "no-autogenerated-uuid-table";
DynamoDbTable mappedTable =
enhancedClient.table(tableName, TableSchema.fromClass(RecordWithoutAutogeneratedUuid.class));
@@ -201,6 +202,7 @@ public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_doesNotRegenerate
mappedTable.putItem(record);
RecordWithoutAutogeneratedUuid retrieved = mappedTable.getItem(
r -> r.key(k -> k.partitionValue("existing-id").sortValue("existing-sk")));
+
assertThat(retrieved.getId()).isEqualTo("existing-id");
assertThat(retrieved.getSortKey()).isEqualTo("existing-sk");
assertThat(retrieved.getGsiPk()).isEqualTo("existing-gsiPk");
@@ -215,7 +217,7 @@ public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_doesNotRegenerate
}
@Test
- public void createBean_givenAutogeneratedUuidAnnotationAppliedOnNonStringAttributeType_throwsException() {
+ public void createBean_whenAutogeneratedUuidAnnotationIsAppliedOnNonStringAttribute_throwsException() {
assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedUuidInvalidTypeRecord.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Uuid attribute")
From 08012f9da054851633a7aa8500969c843016ae82 Mon Sep 17 00:00:00 2001
From: Ana Satirbasa
Date: Thu, 12 Feb 2026 15:40:53 +0200
Subject: [PATCH 07/14] Tests refactoring
---
.../extensions/AutoGeneratedKeyExtension.java | 2 +-
.../AutoGeneratedKeyExtensionTest.java | 150 +++++++++---------
.../AutoGeneratedUuidExtensionTest.java | 5 +-
.../ConflictingAnnotationsTest.java | 6 +-
.../AutoGeneratedKeyRecordTest.java | 21 ++-
.../AutoGeneratedUuidRecordTest.java | 11 +-
.../AutoGeneratedKeyExtensionTest.java | 14 +-
.../AutoGeneratedUuidExtensionTest.java | 5 +-
8 files changed, 111 insertions(+), 103 deletions(-)
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
index 9db27b1ff6d8..d09e7bc47174 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
@@ -139,7 +139,7 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
.ifPresent(attr -> {
throw new IllegalArgumentException(
"@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
- + "primary partition key, primary sort key, or GSI/LSI partition/sort keys."
+ + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. "
+ "Invalid placement on attribute: " + attr);
});
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java
index 5b57765872c6..dd059c9a271c 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java
@@ -109,13 +109,13 @@ public void updateItem_withExistingKey_preservesValueAndDoesNotGenerateNewOne()
Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true);
assertThat(items).hasSize(2);
- WriteModification result =
- extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
- .operationName(OperationName.UPDATE_ITEM)
- .operationContext(PRIMARY_CONTEXT)
- .build());
+ WriteModification result = extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
+ .operationName(OperationName.UPDATE_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
Map transformed = result.transformedItem();
assertThat(transformed).isNotNull().hasSize(2);
@@ -133,13 +133,13 @@ public void updateItem_withoutExistingKey_generatesNewUuid() {
Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true);
assertThat(items).hasSize(1);
- WriteModification result =
- extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
- .operationName(OperationName.UPDATE_ITEM)
- .operationContext(PRIMARY_CONTEXT)
- .build());
+ WriteModification result = extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
+ .operationName(OperationName.UPDATE_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
Map transformed = result.transformedItem();
assertThat(transformed).isNotNull().hasSize(2);
@@ -156,13 +156,13 @@ public void updateItem_withMissingKeyAttribute_insertsGeneratedUuid() {
Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true);
assertThat(items).hasSize(1);
- WriteModification result =
- extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
- .operationName(OperationName.UPDATE_ITEM)
- .operationContext(PRIMARY_CONTEXT)
- .build());
+ WriteModification result = extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
+ .operationName(OperationName.UPDATE_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
assertThat(result.transformedItem()).isNotNull();
assertThat(result.updateExpression()).isNull();
@@ -189,8 +189,9 @@ public void nonStringAttributeAnnotatedWithAutoGeneratedKey_throwsIllegalArgumen
.setter(AutogeneratedKeyItem::setSimpleString))
.build()
)
- .withMessage("Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type "
- + "to be used as a Auto Generated Key attribute. Only String Class type is supported.");
+ .withMessage(
+ "Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type "
+ + "to be used as a Auto Generated Key attribute. Only String Class type is supported.");
}
@Test
@@ -200,13 +201,13 @@ public void autoGeneratedKey_onSecondaryPartitionKey_generatesUuid() {
Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true);
- WriteModification result =
- extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
- .operationName(OperationName.PUT_ITEM)
- .operationContext(PRIMARY_CONTEXT)
- .build());
+ WriteModification result = extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
Map transformed = result.transformedItem();
assertThat(transformed).isNotNull();
@@ -220,13 +221,13 @@ public void autoGeneratedKey_onSecondarySortKey_generatesUuid() {
Map items = LSI_SK_AUTOGEN_SCHEMA.itemToMap(item, true);
- WriteModification result =
- extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(LSI_SK_AUTOGEN_SCHEMA.tableMetadata())
- .operationName(OperationName.PUT_ITEM)
- .operationContext(PRIMARY_CONTEXT)
- .build());
+ WriteModification result = extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(LSI_SK_AUTOGEN_SCHEMA.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build());
Map transformed = result.transformedItem();
assertThat(transformed).isNotNull();
@@ -241,13 +242,13 @@ public void autoGeneratedKey_onNonKeyAttribute_throwsIllegalArgumentException()
Map items = INVALID_NONKEY_AUTOGEN_SCHEMA.itemToMap(item, true);
assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() ->
- extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(INVALID_NONKEY_AUTOGEN_SCHEMA.tableMetadata())
- .operationName(OperationName.PUT_ITEM)
- .operationContext(PRIMARY_CONTEXT)
- .build())
+ .isThrownBy(() -> extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(INVALID_NONKEY_AUTOGEN_SCHEMA.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build())
)
.withMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
+ "primary partition key, primary sort key, or GSI/LSI partition/sort keys.")
@@ -279,16 +280,17 @@ public void conflictingAnnotations_onSameAttribute_throwsIllegalArgumentExceptio
Map items = conflictingSchema.itemToMap(item, true);
assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() ->
- extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(conflictingSchema.tableMetadata())
- .operationName(OperationName.PUT_ITEM)
- .operationContext(PRIMARY_CONTEXT)
- .build())
+ .isThrownBy(() -> extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(conflictingSchema.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build())
)
- .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
- + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ .withMessage(
+ "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
}
@Test
@@ -320,17 +322,16 @@ public void conflictingAnnotations_onSecondaryKey_throwsIllegalArgumentException
Map items = conflictingGsiSchema.itemToMap(item, true);
assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() ->
- extension.beforeWrite(
- DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(conflictingGsiSchema.tableMetadata())
- .operationName(OperationName.PUT_ITEM)
- .operationContext(PRIMARY_CONTEXT)
- .build()))
- .withMessage("Attribute 'keyAttribute' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid "
- + "annotations. "
- + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ .isThrownBy(() -> extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(conflictingGsiSchema.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build()))
+ .withMessage(
+ "Attribute 'keyAttribute' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
}
@Test
@@ -354,17 +355,16 @@ public void conflictDetection_worksRegardlessOfExtensionOrder() {
// Test that the conflict is detected regardless of which extension runs first
assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() ->
- extension.beforeWrite(
- DefaultDynamoDbExtensionContext.builder()
- .items(items)
- .tableMetadata(conflictingSchema.tableMetadata())
- .operationName(OperationName.PUT_ITEM)
- .operationContext(PRIMARY_CONTEXT)
- .build()))
- .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid "
- + "annotations. "
- + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ .isThrownBy(() -> extension.beforeWrite(
+ DefaultDynamoDbExtensionContext.builder()
+ .items(items)
+ .tableMetadata(conflictingSchema.tableMetadata())
+ .operationName(OperationName.PUT_ITEM)
+ .operationContext(PRIMARY_CONTEXT)
+ .build()))
+ .withMessage(
+ "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
}
@Test
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
index d506fced8d6f..33da228c5472 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java
@@ -180,8 +180,9 @@ void IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() {
.setter(ItemWithUuid::setSimpleString))
.build())
- .withMessage("Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type"
- + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported.");
+ .withMessage(
+ "Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type"
+ + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported.");
}
@Test
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java
index 7b7d01c7afce..456650ec706f 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java
@@ -76,9 +76,9 @@ public void autogeneratedKeyExtensionFirst_detectsConflictWithUuidExtension() {
.operationContext(PRIMARY_CONTEXT)
.build())
)
- .withMessage("Attribute 'autogeneratedKeyField' cannot have both "
- + "@DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
- + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ .withMessage(
+ "Attribute 'autogeneratedKeyField' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid "
+ + "annotations. These annotations have conflicting behaviors and cannot be used together on the same attribute.");
}
@Test
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
index ff811115937b..f2b3283b0650 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
@@ -48,12 +48,9 @@
/**
* Tests for @DynamoDbAutoGeneratedKey annotation functionality.
*
- * Tests cover:
- * - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK)
- * - UpdateBehavior control (WRITE_ALWAYS vs WRITE_IF_NOT_EXISTS) for secondary index keys
- * - Primary key limitations (UpdateBehavior has no effect)
- * - Error handling for invalid usage
- * - Integration with other extensions (VersionedRecord)
+ * Tests cover: - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK) - UpdateBehavior control (WRITE_ALWAYS vs
+ * WRITE_IF_NOT_EXISTS) for secondary index keys - Primary key limitations (UpdateBehavior has no effect) - Error handling for
+ * invalid usage - Integration with other extensions (VersionedRecord)
*/
@RunWith(Parameterized.class)
public class AutoGeneratedKeyRecordTest extends LocalDynamoDbSyncTestBase {
@@ -189,8 +186,10 @@ public void putItem_withAutogeneratedKeySetOnNonKeyAttribute_throwsException() {
Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> table.putItem(record))
- .withMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes")
- .withMessageContaining("notAKey");
+ .withMessage(
+ "@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
+ + "primary partition key, primary sort key, or GSI/LSI partition/sort keys."
+ + "Invalid placement on attribute: notAKey");
} finally {
deleteTableByName(tableName);
}
@@ -252,9 +251,9 @@ public void autogeneratedConflictingAnnotations_throwsException() {
Assertions
.assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> table.putItem(record))
- .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid "
- + "annotations. "
- + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ .withMessage(
+ "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
} finally {
deleteTableByName(tableName);
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java
index d513df433c35..161cf7b30fd5 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java
@@ -16,6 +16,7 @@
package software.amazon.awssdk.enhanced.dynamodb.functionaltests;
import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute;
import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue;
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;
@@ -328,11 +329,11 @@ public void putItem_onRecordWithAutogeneratedConflictingAnnotations_throwsExcept
ConflictingAnnotationsRecord record = new ConflictingAnnotationsRecord();
record.setPayload("payload");
- Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
- .isThrownBy(() -> table.putItem(record))
- .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid "
- + "annotations. These annotations have conflicting behaviors and cannot be used together "
- + "on the same attribute.");
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> table.putItem(record))
+ .withMessage(
+ "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
} finally {
getDynamoDbClient().deleteTable(DeleteTableRequest.builder().tableName(tableName).build());
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
index 6e24fe2d96e0..2e1ca89a0830 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
@@ -278,7 +278,9 @@ public void putItem_whenAnnotationInConflictWithAutogeneratedUuidAnnotation_thro
assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyConflictingRecord()))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid");
+ .hasMessage(
+ "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
} finally {
try {
mappedTable.deleteTable();
@@ -298,7 +300,10 @@ public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() {
assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyOnNonKeyAttributeRecord()))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes");
+ .hasMessage(
+ "@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
+ + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. "
+ + "Invalid placement on attribute: nonKeyAttribute");
} finally {
try {
mappedTable.deleteTable();
@@ -342,8 +347,9 @@ public void putItem_whenNoAutogeneratedKeyAnnotationIsPresent_doesNotRegenerateU
public void createBean_givenAutogeneratedKeyAnnotationAppliedOnNonStringAttributeType_throwsException() {
assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedKeyInvalidTypeRecord.class))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Key attribute")
- .hasMessageContaining("Only String Class type is supported");
+ .hasMessage(
+ "Attribute 'id' of Class type class java.lang.Integer is not a suitable Java Class type to be used "
+ + "as a Auto Generated Key attribute. Only String Class type is supported.");
}
@DynamoDbBean
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java
index c9a29f0e4563..974d638ed8d9 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java
@@ -220,8 +220,9 @@ public void putItem_whenAutogeneratedUuidAnnotationIsNotPresent_doesNotRegenerat
public void createBean_whenAutogeneratedUuidAnnotationIsAppliedOnNonStringAttribute_throwsException() {
assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedUuidInvalidTypeRecord.class))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Uuid attribute")
- .hasMessageContaining("Only String Class type is supported");
+ .hasMessage(
+ "Attribute 'id' of Class type class java.lang.Integer is not a suitable Java Class type to be used "
+ + "as a Auto Generated Uuid attribute. Only String Class type is supported.");
}
@DynamoDbBean
From 67cd161eb9cb71f3c5c654761c7927350dbe6a0f Mon Sep 17 00:00:00 2001
From: Ana Satirbasa
Date: Fri, 13 Feb 2026 10:32:03 +0200
Subject: [PATCH 08/14] Addressed PR feedback and refactored tests
---
.../extensions/AutoGeneratedKeyExtension.java | 172 +++++++++++-------
.../AutoGeneratedUuidExtension.java | 51 ++++--
.../extensions/ExtensionsValidationUtils.java | 74 ++++++++
.../AutoGeneratedKeyExtensionTest.java | 8 +-
...ogeneratedConflictingAnnotationsTest.java} | 4 +-
.../ExtensionsValidationUtilsTest.java | 92 ++++++++++
.../AutoGeneratedKeyRecordTest.java | 16 +-
.../AutoGeneratedUuidRecordTest.java | 2 +-
.../AutoGeneratedKeyExtensionTest.java | 2 +-
9 files changed, 321 insertions(+), 100 deletions(-)
create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java
rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/{ConflictingAnnotationsTest.java => AutogeneratedConflictingAnnotationsTest.java} (99%)
create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtilsTest.java
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
index d09e7bc47174..43a1943b521a 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
@@ -15,6 +15,8 @@
package software.amazon.awssdk.enhanced.dynamodb.extensions;
+import static java.util.Collections.newSetFromMap;
+
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -23,6 +25,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
@@ -39,65 +42,86 @@
import software.amazon.awssdk.utils.Validate;
/**
- * Generates a random UUID (via {@link java.util.UUID#randomUUID()}) for any attribute tagged with
- * {@code @DynamoDbAutoGeneratedKey} when that attribute is missing or empty on a write (put/update).
- *
- * Key Difference from @DynamoDbAutoGeneratedUuid: This extension only generates UUIDs when the
- * attribute value is null or empty, preserving existing values. In contrast, {@code @DynamoDbAutoGeneratedUuid} always generates
- * new UUIDs regardless of existing values.
- *
- * Conflict Detection: This extension cannot be used together with {@code @DynamoDbAutoGeneratedUuid} on the same
- * attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown at runtime to
- * prevent unpredictable behavior based on extension load order.
- *
- * The annotation may be placed only on key attributes:
+ * Generates a random UUID (via {@link java.util.UUID#randomUUID()}) for attributes tagged with {@code @DynamoDbAutoGeneratedKey}
+ * when the attribute value is missing or empty during write operations (put, update, batch write, or transact write).
+ *
+ *
Difference from {@code @DynamoDbAutoGeneratedUuid}:
+ * This extension generates a UUID only when the attribute is null or empty, preserving existing values. In contrast,
+ * {@code @DynamoDbAutoGeneratedUuid} always generates a new UUID regardless of the current value.
+ *
+ *
Conflict Detection:
+ * {@code @DynamoDbAutoGeneratedKey} and {@code @DynamoDbAutoGeneratedUuid} cannot be applied to the same attribute. If both
+ * annotations are present, an {@link IllegalArgumentException} is thrown during schema validation.
+ *
+ *
Supported Attributes:
+ * The annotation may only be applied to key attributes:
*
- * - Primary partition key (PK) or primary sort key (SK)
- * - Partition key or sort key of any secondary index (GSI or LSI)
+ * - Primary partition key (PK)
+ * - Primary sort key (SK)
+ * - Partition or sort keys of secondary indexes (GSI or LSI)
*
*
- * Validation: The extension enforces this at runtime during {@link #beforeWrite} by comparing the
- * annotated attributes against the table's known key attributes. If an annotated attribute
- * is not a PK/SK or an GSI/LSI, an {@link IllegalArgumentException} is thrown.
+ * Validation Behavior:
+ * Annotation conflict detection and key-placement validation are performed once per {@link TableMetadata}
+ * instance and cached to avoid repeated validation on subsequent writes.
*
- *
UpdateBehavior Limitations: {@code @DynamoDbUpdateBehavior} has no effect on primary keys due to
- * DynamoDB's UpdateItem API requirements. It only affects secondary index keys.
+ * UpdateBehavior Limitation:
+ * {@code @DynamoDbUpdateBehavior} does not apply to primary keys due to DynamoDB API constraints.
+ * It only affects secondary index keys.
*/
@SdkPublicApi
@ThreadSafe
public final class AutoGeneratedKeyExtension implements DynamoDbEnhancedClientExtension {
/**
- * Custom metadata key under which we store the set of annotated attribute names.
+ * Metadata keys used to record attributes annotated with {@code @DynamoDbAutoGeneratedKey} and
+ * {@code @DynamoDbAutoGeneratedUuid}. These are used during schema validation to detect annotation conflicts.
*/
private static final String CUSTOM_METADATA_KEY =
"software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute";
+ private static final String UUID_EXTENSION_METADATA_KEY =
+ "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute";
+
+ private static final StaticAttributeTag AUTO_GENERATED_KEY_ATTRIBUTE = new AutoGeneratedKeyAttribute();
+ private static final String AUTOGENERATED_KEY_ANNOTATION = "@DynamoDbAutoGeneratedKey";
+ private static final String AUTOGENERATED_UUID_ANNOTATION = "@DynamoDbAutoGeneratedUuid";
/**
- * Metadata key used by AutoGeneratedUuidExtension to detect conflicts.
+ * Stores the TableMetadata instances that have already been validated by this extension. Uses a ConcurrentHashMap to ensure
+ * thread-safe access during concurrent write operations.
*/
- private static final String UUID_EXTENSION_METADATA_KEY =
- "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute";
+ private final Set validatedSchemas = newSetFromMap(new ConcurrentHashMap<>());
- private static final AutoGeneratedKeyAttribute AUTO_GENERATED_KEY_ATTRIBUTE = new AutoGeneratedKeyAttribute();
+ /**
+ * Caches the set of valid key attribute names per TableMetadata instance. Computed once per schema.
+ */
+ private final Map> allowedKeysCache = new ConcurrentHashMap<>();
private AutoGeneratedKeyExtension() {
}
+ /**
+ * @return an Instance of {@link AutoGeneratedKeyExtension}
+ */
+ public static AutoGeneratedKeyExtension create() {
+ return new AutoGeneratedKeyExtension();
+ }
+
public static Builder builder() {
return new Builder();
}
/**
- * If this table has attributes tagged for auto-generation, insert a UUID value into the outgoing item for any such attribute
- * that is currently missing/empty. Unlike {@code @DynamoDbAutoGeneratedUuid}, this preserves existing values.
- *
- * Also validates that the annotation is only used on PK/SK/GSI/LSI key attributes and that there are no conflicts with
+ * Inserts a UUID for attributes tagged with {@code @DynamoDbAutoGeneratedKey} when the attribute is null or empty, preserving
+ * existing values.
*
- * @DynamoDbAutoGeneratedUuid.
+ *
Schema-level validation (annotation conflict detection and key-placement checks)
+ * is executed once per schema instance and cached.
*/
@Override
public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
+
+
Collection taggedAttributes = context.tableMetadata()
.customMetadataObject(CUSTOM_METADATA_KEY, Collection.class)
.orElse(null);
@@ -106,44 +130,12 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
return WriteModification.builder().build();
}
- // Check for conflicts with @DynamoDbAutoGeneratedUuid
- Collection uuidTaggedAttributes = context.tableMetadata()
- .customMetadataObject(UUID_EXTENSION_METADATA_KEY, Collection.class)
- .orElse(Collections.emptyList());
-
- taggedAttributes.stream()
- .filter(uuidTaggedAttributes::contains)
- .findFirst()
- .ifPresent(attribute -> {
- throw new IllegalArgumentException(
- "Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and "
- + "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors "
- + "and cannot be used together on the same attribute.");
- });
-
- TableMetadata metadata = context.tableMetadata();
- Set allowedKeys = new HashSet<>();
-
- // ensure every @DynamoDbAutoGeneratedKey attribute is a PK/SK or GSI/LSI. If not, throw IllegalArgumentException
- allowedKeys.add(metadata.primaryPartitionKey());
- metadata.primarySortKey().ifPresent(allowedKeys::add);
-
- metadata.indices().stream().map(IndexMetadata::name).forEach(indexName -> {
- allowedKeys.add(metadata.indexPartitionKey(indexName));
- metadata.indexSortKey(indexName).ifPresent(allowedKeys::add);
- });
+ TableMetadata tableMetadata = context.tableMetadata();
+ if (validatedSchemas.add(tableMetadata)) {
+ validateNoAutoGeneratedAnnotationConflict(tableMetadata);
+ validateAutoGeneratedKeyPlacement(tableMetadata, taggedAttributes);
+ }
- taggedAttributes.stream()
- .filter(attr -> !allowedKeys.contains(attr))
- .findFirst()
- .ifPresent(attr -> {
- throw new IllegalArgumentException(
- "@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
- + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. "
- + "Invalid placement on attribute: " + attr);
- });
-
- // Generate UUIDs for missing/empty annotated attributes
Map itemToTransform = new HashMap<>(context.items());
taggedAttributes.forEach(attr -> insertUuidIfMissing(itemToTransform, attr));
@@ -152,6 +144,52 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
.build();
}
+ /**
+ * Validates (once per TableMetadata instance) that @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid are not applied
+ * to the same attribute.
+ */
+ private static void validateNoAutoGeneratedAnnotationConflict(TableMetadata tableMetadata) {
+ ExtensionsValidationUtils.validateNoAnnotationConflict(
+ tableMetadata,
+ CUSTOM_METADATA_KEY,
+ UUID_EXTENSION_METADATA_KEY,
+ AUTOGENERATED_KEY_ANNOTATION,
+ AUTOGENERATED_UUID_ANNOTATION
+ );
+ }
+
+ /**
+ * Validates that all attributes tagged with @DynamoDbAutoGeneratedKey are either primary keys or secondary index keys.
+ **/
+ private void validateAutoGeneratedKeyPlacement(TableMetadata tableMetadata,
+ Collection taggedAttributeNames) {
+
+ Set allowedKeyAttributes = allowedKeysCache.computeIfAbsent(tableMetadata, metadata -> {
+ Set keyAttributes = new HashSet<>();
+ keyAttributes.add(metadata.primaryPartitionKey());
+ metadata.primarySortKey().ifPresent(keyAttributes::add);
+
+ metadata.indices().stream()
+ .map(IndexMetadata::name)
+ .forEach(indexName -> {
+ keyAttributes.add(metadata.indexPartitionKey(indexName));
+ metadata.indexSortKey(indexName).ifPresent(keyAttributes::add);
+ });
+
+ return keyAttributes;
+ });
+
+ taggedAttributeNames.stream()
+ .filter(attrName -> !allowedKeyAttributes.contains(attrName))
+ .findFirst()
+ .ifPresent(invalidAttribute -> {
+ throw new IllegalArgumentException(
+ "@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
+ + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. "
+ + "Invalid placement on attribute: " + invalidAttribute);
+ });
+ }
+
private void insertUuidIfMissing(Map itemToTransform, String key) {
AttributeValue existing = itemToTransform.get(key);
if (Objects.isNull(existing) || StringUtils.isBlank(existing.s())) {
@@ -210,7 +248,7 @@ public void validateType(String attributeName,
@Override
public Consumer modifyMetadata(String attributeName,
AttributeValueType attributeValueType) {
- // Record the names of the attributes annotated with @DynamoDbAutoGeneratedKey for later lookup in beforeWrite()
+ // Records attribute names annotated with @DynamoDbAutoGeneratedKey for lookup during write processing.
return metadata -> metadata.addCustomMetadataObject(
CUSTOM_METADATA_KEY, Collections.singleton(attributeName));
}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
index 884e12c20f89..e3f7b8922d07 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
@@ -15,11 +15,15 @@
package software.amazon.awssdk.enhanced.dynamodb.extensions;
+import static java.util.Collections.newSetFromMap;
+
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
@@ -27,6 +31,7 @@
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
+import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
@@ -85,17 +90,24 @@
@SdkPublicApi
@ThreadSafe
public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientExtension {
- private static final String CUSTOM_METADATA_KEY =
- "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute";
/**
- * Metadata key used by AutoGeneratedKeyExtension to detect conflicts.
+ * Custom metadata keys under which AutoGeneratedUuidExtension/AutoGeneratedKeyExtension record attributes annotated with
+ * {@code @DynamoDbAutoGeneratedUuid}/{@code @DynamoDbAutoGeneratedKey}. Used to detect conflicts during schema validation.
*/
- private static final String KEY_EXTENSION_METADATA_KEY =
+ private static final String CUSTOM_METADATA_KEY =
+ "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute";
+ private static final String AUTOGENERATED_KEY_EXTENSION_METADATA_KEY =
"software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute";
private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute();
+ /**
+ * Stores the TableMetadata instances that have already been validated by this extension. Uses a ConcurrentHashMap to ensure
+ * thread-safe access during concurrent write operations.
+ */
+ private final Set validatedSchemas = newSetFromMap(new ConcurrentHashMap<>());
+
private AutoGeneratedUuidExtension() {
}
@@ -124,20 +136,8 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
return WriteModification.builder().build();
}
- // Check for conflicts with @DynamoDbAutoGeneratedKey
- Collection keyTaggedAttributes = context.tableMetadata()
- .customMetadataObject(KEY_EXTENSION_METADATA_KEY, Collection.class)
- .orElse(Collections.emptyList());
-
- customMetadataObject.stream()
- .filter(keyTaggedAttributes::contains)
- .findFirst()
- .ifPresent(attribute -> {
- throw new IllegalArgumentException(
- "Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and "
- + "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors "
- + "and cannot be used together on the same attribute.");
- });
+ TableMetadata metadata = context.tableMetadata();
+ validateNoAnnotationConflict(metadata);
Map itemToTransform = new HashMap<>(context.items());
customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key));
@@ -146,6 +146,21 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
.build();
}
+ /**
+ * Validates (once per TableMetadata instance) that @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid are not applied
+ * to the same attribute.
+ */
+ private void validateNoAnnotationConflict(TableMetadata tableMetadata) {
+ if (validatedSchemas.add(tableMetadata)) {
+ ExtensionsValidationUtils.validateNoAnnotationConflict(
+ tableMetadata,
+ AUTOGENERATED_KEY_EXTENSION_METADATA_KEY,
+ CUSTOM_METADATA_KEY,
+ "@DynamoDbAutoGeneratedKey",
+ "@DynamoDbAutoGeneratedUuid");
+ }
+ }
+
private void insertUuidInItemToTransform(Map itemToTransform,
String key) {
itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build());
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java
new file mode 100644
index 000000000000..1b7a81e2ed6a
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java
@@ -0,0 +1,74 @@
+/*
+ * 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.extensions;
+
+import java.util.Collection;
+import java.util.Collections;
+import software.amazon.awssdk.annotations.SdkProtectedApi;
+import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
+
+/**
+ * Provides shared schema validation utilities for DynamoDB enhanced client extensions.
+ */
+@SdkProtectedApi
+public final class ExtensionsValidationUtils {
+
+ private ExtensionsValidationUtils() {
+ }
+
+ /**
+ * Validates that there are no attributes that have both annotations. These annotations have conflicting behaviors and cannot
+ * be used together on the same attribute. If an attribute is found with both annotations, an IllegalArgumentException is
+ * thrown with a message indicating the attribute and the conflicting annotations.
+ *
+ * @param tableMetadata The metadata of the table to validate.
+ * @param firstMetadataKey The metadata key for the first annotation to check for.
+ * @param secondMetadataKey The metadata key for the second annotation to check for.
+ * @param firstAnnotationName The name of the first annotation to use in the error message if a conflict is found.
+ * @param secondAnnotationName The name of the second annotation to use in the error message if a conflict is found.
+ */
+ public static void validateNoAnnotationConflict(TableMetadata tableMetadata,
+ String firstMetadataKey,
+ String secondMetadataKey,
+ String firstAnnotationName,
+ String secondAnnotationName) {
+
+ Collection> attributesWithFirstAnnotation =
+ tableMetadata.customMetadataObject(firstMetadataKey, Collection.class).orElse(Collections.emptyList());
+
+ if (attributesWithFirstAnnotation.isEmpty()) {
+ return;
+ }
+
+ Collection> attributesWithSecondAnnotation =
+ tableMetadata.customMetadataObject(secondMetadataKey, Collection.class).orElse(Collections.emptyList());
+
+ if (attributesWithSecondAnnotation.isEmpty()) {
+ return;
+ }
+
+ attributesWithFirstAnnotation
+ .stream()
+ .filter(attributesWithSecondAnnotation::contains)
+ .findFirst()
+ .ifPresent(attribute -> {
+ throw new IllegalArgumentException(
+ "Attribute '" + attribute + "' cannot have both " + firstAnnotationName
+ + " and " + secondAnnotationName + " annotations. "
+ + "These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ });
+ }
+}
\ No newline at end of file
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java
index dd059c9a271c..0720ffa99d8c 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java
@@ -34,7 +34,7 @@ public class AutoGeneratedKeyExtensionTest {
private static final OperationContext PRIMARY_CONTEXT =
DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName());
- private final AutoGeneratedKeyExtension extension = AutoGeneratedKeyExtension.builder().build();
+ private final AutoGeneratedKeyExtension extension = AutoGeneratedKeyExtension.create();
/**
* Schema that places @DynamoDbAutoGeneratedKey on GSI key ("keyAttribute") -> the validation passes.
@@ -250,9 +250,9 @@ public void autoGeneratedKey_onNonKeyAttribute_throwsIllegalArgumentException()
.operationContext(PRIMARY_CONTEXT)
.build())
)
- .withMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
- + "primary partition key, primary sort key, or GSI/LSI partition/sort keys.")
- .withMessageContaining("keyAttribute");
+ .withMessage("@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
+ + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. "
+ + "Invalid placement on attribute: keyAttribute");
}
@Test
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutogeneratedConflictingAnnotationsTest.java
similarity index 99%
rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java
rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutogeneratedConflictingAnnotationsTest.java
index 456650ec706f..933bc9f64f86 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutogeneratedConflictingAnnotationsTest.java
@@ -34,14 +34,14 @@
* Tests to verify that conflicting annotations (@DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid) are properly detected
* and throw exceptions regardless of extension load order.
*/
-public class ConflictingAnnotationsTest {
+public class AutogeneratedConflictingAnnotationsTest {
private static final String RECORD_ID = "1";
private static final String TABLE_NAME = "table-name";
private static final OperationContext PRIMARY_CONTEXT =
DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName());
- private final AutoGeneratedKeyExtension keyExtension = AutoGeneratedKeyExtension.builder().build();
+ private final AutoGeneratedKeyExtension keyExtension = AutoGeneratedKeyExtension.create();
private final AutoGeneratedUuidExtension uuidExtension = AutoGeneratedUuidExtension.create();
/**
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtilsTest.java
new file mode 100644
index 000000000000..bc78b5b45f06
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtilsTest.java
@@ -0,0 +1,92 @@
+package software.amazon.awssdk.enhanced.dynamodb.extensions;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.when;
+import static software.amazon.awssdk.enhanced.dynamodb.extensions.ExtensionsValidationUtils.validateNoAnnotationConflict;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ExtensionsValidationUtilsTest {
+
+ @Mock
+ private TableMetadata metadata;
+
+ private static final String AUTOGENERATED_KEY_METADATA_KEY =
+ "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute";
+ private static final String AUTOGENERATED_UUID_METADATA_KEY =
+ "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute";
+
+ private static final String AUTOGENERATED_KEY_ANNOTATION = "@DynamoDbAutoGeneratedKey";
+ private static final String AUTOGENERATED_UUID_ANNOTATION = "@DynamoDbAutoGeneratedUuid";
+
+ @Test
+ public void validateNoAnnotationConflict_whenAnnotationsOverlap_throwsException() {
+ when(metadata.customMetadataObject(AUTOGENERATED_KEY_METADATA_KEY, Collection.class))
+ .thenReturn(Optional.of(Collections.singleton("sharedAttribute")));
+ when(metadata.customMetadataObject(AUTOGENERATED_UUID_METADATA_KEY, Collection.class))
+ .thenReturn(Optional.of(Arrays.asList("sharedAttribute", "otherUuidAttribute")));
+
+ assertThatThrownBy(() -> validateNoAnnotationConflict(
+ metadata,
+ AUTOGENERATED_KEY_METADATA_KEY,
+ AUTOGENERATED_UUID_METADATA_KEY,
+ AUTOGENERATED_KEY_ANNOTATION,
+ AUTOGENERATED_UUID_ANNOTATION
+ ))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(
+ "Attribute 'sharedAttribute' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid "
+ + "annotations. These annotations have conflicting behaviors and cannot be used together on the same attribute.");
+ }
+
+ @Test
+ public void validateNoAnnotationConflict_whenNoAnnotatedFields_doesNotThrowException() {
+ assertThatCode(() -> validateNoAnnotationConflict(
+ metadata,
+ AUTOGENERATED_KEY_METADATA_KEY,
+ AUTOGENERATED_UUID_METADATA_KEY,
+ AUTOGENERATED_KEY_ANNOTATION,
+ AUTOGENERATED_UUID_ANNOTATION
+ )).doesNotThrowAnyException();
+ }
+
+ @Test
+ public void validateNoAnnotationConflict_whenAnnotationsDontOverlap_doesNotThrowException() {
+ when(metadata.customMetadataObject(AUTOGENERATED_KEY_METADATA_KEY, Collection.class))
+ .thenReturn(Optional.of(Collections.singleton("keyAttribute")));
+ when(metadata.customMetadataObject(AUTOGENERATED_UUID_METADATA_KEY, Collection.class))
+ .thenReturn(Optional.of(Collections.singleton("uuidAttribute")));
+
+ assertThatCode(() -> validateNoAnnotationConflict(
+ metadata,
+ AUTOGENERATED_KEY_METADATA_KEY,
+ AUTOGENERATED_UUID_METADATA_KEY,
+ AUTOGENERATED_KEY_ANNOTATION,
+ AUTOGENERATED_UUID_ANNOTATION
+ )).doesNotThrowAnyException();
+ }
+
+ @Test
+ public void validateNoAnnotationConflict_whenSecondAnnotationIsNotUsed_doesNotThrow() {
+ when(metadata.customMetadataObject(AUTOGENERATED_KEY_METADATA_KEY, Collection.class))
+ .thenReturn(Optional.empty());
+
+ assertThatCode(() -> validateNoAnnotationConflict(
+ metadata,
+ AUTOGENERATED_KEY_METADATA_KEY,
+ AUTOGENERATED_UUID_METADATA_KEY,
+ AUTOGENERATED_KEY_ANNOTATION,
+ AUTOGENERATED_UUID_ANNOTATION
+ )).doesNotThrowAnyException();
+ }
+}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
index f2b3283b0650..2902465297f2 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
@@ -60,7 +60,7 @@ public class AutoGeneratedKeyRecordTest extends LocalDynamoDbSyncTestBase {
public AutoGeneratedKeyRecordTest(String testName, TableSchema schema) {
this.mappedTable = DynamoDbEnhancedClient.builder()
.dynamoDbClient(getDynamoDbClient())
- .extensions(AutoGeneratedKeyExtension.builder().build())
+ .extensions(AutoGeneratedKeyExtension.create())
.build()
.table(getConcreteTableName("AutoGenKey-table"), schema);
}
@@ -188,7 +188,7 @@ public void putItem_withAutogeneratedKeySetOnNonKeyAttribute_throwsException() {
.isThrownBy(() -> table.putItem(record))
.withMessage(
"@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
- + "primary partition key, primary sort key, or GSI/LSI partition/sort keys."
+ + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. "
+ "Invalid placement on attribute: notAKey");
} finally {
deleteTableByName(tableName);
@@ -201,8 +201,9 @@ public void versionedRecord_worksWithAutoGeneratedKeys() {
String tableName = getConcreteTableName("versioned-test");
DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder()
.dynamoDbClient(getDynamoDbClient())
- .extensions(AutoGeneratedKeyExtension.builder().build(),
- VersionedRecordExtension.builder().build())
+ .extensions(
+ AutoGeneratedKeyExtension.builder().build(),
+ VersionedRecordExtension.builder().build())
.build();
DynamoDbTable table = client.table(tableName, TableSchema.fromBean(VersionedRecord.class));
@@ -236,8 +237,9 @@ public void autogeneratedConflictingAnnotations_throwsException() {
String tableName = getConcreteTableName("conflicting-annotations-test");
DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder()
.dynamoDbClient(getDynamoDbClient())
- .extensions(AutoGeneratedKeyExtension.builder().build(),
- AutoGeneratedUuidExtension.create())
+ .extensions(
+ AutoGeneratedKeyExtension.create(),
+ AutoGeneratedUuidExtension.create())
.build();
try {
@@ -376,7 +378,7 @@ public void batchWrite_mixedPutDeleteOperations() {
private DynamoDbEnhancedClient createClient() {
return DynamoDbEnhancedClient.builder()
.dynamoDbClient(getDynamoDbClient())
- .extensions(AutoGeneratedKeyExtension.builder().build())
+ .extensions(AutoGeneratedKeyExtension.create())
.build();
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java
index 161cf7b30fd5..71f79bf7851e 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java
@@ -318,7 +318,7 @@ public void putItem_onRecordWithAutogeneratedConflictingAnnotations_throwsExcept
DynamoDbEnhancedClient client =
DynamoDbEnhancedClient.builder()
.dynamoDbClient(getDynamoDbClient())
- .extensions(AutoGeneratedUuidExtension.create(), AutoGeneratedKeyExtension.builder().build())
+ .extensions(AutoGeneratedUuidExtension.create(), AutoGeneratedKeyExtension.create())
.build();
try {
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
index 2e1ca89a0830..de98bb27c519 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
@@ -54,7 +54,7 @@ public class AutoGeneratedKeyExtensionTest extends LocalDynamoDbSyncTestBase {
.dynamoDbClient(getDynamoDbClient())
.extensions(Stream.concat(
ExtensionResolver.defaultExtensions().stream(),
- Stream.of(AutoGeneratedKeyExtension.builder().build()))
+ Stream.of(AutoGeneratedKeyExtension.create()))
.collect(Collectors.toList()))
.build();
From a4bfb1e6134f0246447b64da5e249cbb6ad00af4 Mon Sep 17 00:00:00 2001
From: Ana Satirbasa
Date: Fri, 13 Feb 2026 12:11:20 +0200
Subject: [PATCH 09/14] Addressed PR feedback
---
.../AutoGeneratedUuidExtension.java | 6 ++-
.../extensions/ExtensionsValidationUtils.java | 24 +++++-----
.../annotations/DynamoDbAutoGeneratedKey.java | 47 ++++---------------
.../DynamoDbAutoGeneratedUuid.java | 18 +++++--
4 files changed, 41 insertions(+), 54 deletions(-)
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
index e3f7b8922d07..9c8e55d82a10 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
@@ -101,6 +101,8 @@ public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientE
"software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute";
private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute();
+ private static final String AUTOGENERATED_KEY_ANNOTATION = "@DynamoDbAutoGeneratedKey";
+ private static final String AUTOGENERATED_UUID_ANNOTATION = "@DynamoDbAutoGeneratedUuid";
/**
* Stores the TableMetadata instances that have already been validated by this extension. Uses a ConcurrentHashMap to ensure
@@ -156,8 +158,8 @@ private void validateNoAnnotationConflict(TableMetadata tableMetadata) {
tableMetadata,
AUTOGENERATED_KEY_EXTENSION_METADATA_KEY,
CUSTOM_METADATA_KEY,
- "@DynamoDbAutoGeneratedKey",
- "@DynamoDbAutoGeneratedUuid");
+ AUTOGENERATED_KEY_ANNOTATION,
+ AUTOGENERATED_UUID_ANNOTATION);
}
}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java
index 1b7a81e2ed6a..4a04286e2ece 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java
@@ -35,34 +35,34 @@ private ExtensionsValidationUtils() {
* thrown with a message indicating the attribute and the conflicting annotations.
*
* @param tableMetadata The metadata of the table to validate.
- * @param firstMetadataKey The metadata key for the first annotation to check for.
- * @param secondMetadataKey The metadata key for the second annotation to check for.
+ * @param firstAnnotationMetadataKey The metadata key for the first annotation to check for.
+ * @param secondAnnotationMetadataKey The metadata key for the second annotation to check for.
* @param firstAnnotationName The name of the first annotation to use in the error message if a conflict is found.
* @param secondAnnotationName The name of the second annotation to use in the error message if a conflict is found.
*/
public static void validateNoAnnotationConflict(TableMetadata tableMetadata,
- String firstMetadataKey,
- String secondMetadataKey,
+ String firstAnnotationMetadataKey,
+ String secondAnnotationMetadataKey,
String firstAnnotationName,
String secondAnnotationName) {
- Collection> attributesWithFirstAnnotation =
- tableMetadata.customMetadataObject(firstMetadataKey, Collection.class).orElse(Collections.emptyList());
+ Collection> attributesHavingFirstAnnotation =
+ tableMetadata.customMetadataObject(firstAnnotationMetadataKey, Collection.class).orElse(Collections.emptyList());
- if (attributesWithFirstAnnotation.isEmpty()) {
+ if (attributesHavingFirstAnnotation.isEmpty()) {
return;
}
- Collection> attributesWithSecondAnnotation =
- tableMetadata.customMetadataObject(secondMetadataKey, Collection.class).orElse(Collections.emptyList());
+ Collection> attributesHavingSecondAnnotation =
+ tableMetadata.customMetadataObject(secondAnnotationMetadataKey, Collection.class).orElse(Collections.emptyList());
- if (attributesWithSecondAnnotation.isEmpty()) {
+ if (attributesHavingSecondAnnotation.isEmpty()) {
return;
}
- attributesWithFirstAnnotation
+ attributesHavingFirstAnnotation
.stream()
- .filter(attributesWithSecondAnnotation::contains)
+ .filter(attributesHavingSecondAnnotation::contains)
.findFirst()
.ifPresent(attribute -> {
throw new IllegalArgumentException(
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java
index 6fbb90d5a0c8..cdad8d30616b 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java
@@ -27,47 +27,20 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
/**
- * Annotation that marks a key attribute to be automatically populated with a random UUID if no value is provided during a write
- * operation (put or update). This annotation is intended to work specifically with key attributes.
+ * Marks a key attribute to be automatically populated with a UUID when the value is null or empty during a write operation.
*
- * This annotation is designed for use with the V2 {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}.
- * It is registered via {@link BeanTableSchemaAttributeTag} and its behavior is implemented by
- * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension}.
+ * Usage: May only be applied to key attributes: primary partition key, primary sort key, or secondary index
+ * (GSI/LSI) keys. If used on a non-key attribute, {@code AutoGeneratedKeyExtension} throws an {@link IllegalArgumentException}
+ * during schema validation.
*
- *
Where this annotation can be applied
- * This annotation is only valid on attributes that serve as keys:
- *
- * - The table's primary partition key or sort key
- * - The partition key or sort key of a secondary index (GSI or LSI)
- *
- * If applied to any other attribute, the {@code AutoGeneratedKeyExtension} will throw an
- * {@link IllegalArgumentException} at runtime.
+ * Semantics: Generates a UUID using {@link java.util.UUID#randomUUID()}
+ * only when the attribute is absent. Existing values are preserved.
*
- *
How values are generated
- *
- * - On writes where the annotated attribute is null or empty, a new UUID value is generated
- * using {@link java.util.UUID#randomUUID()}.
- * - If a value is already set on the attribute, that value is preserved and not replaced.
- * - This behavior differs from {@code @DynamoDbAutoGeneratedUuid}, which always generates new UUIDs regardless of existing
- * values.
- *
+ * Difference from {@code @DynamoDbAutoGeneratedUuid}:
+ * This annotation is intended for key attributes and generates a value only when missing. {@code @DynamoDbAutoGeneratedUuid} can
+ * be applied to any attribute and always generates a new UUID on every write.
*
- *
Behavior with UpdateBehavior
- * Primary Keys: {@link DynamoDbUpdateBehavior} has no effect on primary partition keys
- * or primary sort keys. Primary keys are immutable in DynamoDB and cannot use conditional update behaviors like
- * {@link UpdateBehavior#WRITE_IF_NOT_EXISTS}. UUIDs will be generated whenever the primary key attribute is missing
- * or empty, regardless of any {@code UpdateBehavior} setting.
- *
- * Secondary Index Keys: For GSI/LSI keys, {@link DynamoDbUpdateBehavior} can be used:
- *
- * - {@link UpdateBehavior#WRITE_ALWAYS} (default) – Generate a new UUID whenever the attribute is missing during write.
- * - {@link UpdateBehavior#WRITE_IF_NOT_EXISTS} – Generate a UUID only on the first write, preserving the value on
- * subsequent updates.
- *
- *
- *
- * Type restriction
- * This annotation is only valid on attributes of type {@link String}.
+ * Valid only for {@link String} attributes.
*/
@SdkPublicApi
@Documented
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedUuid.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedUuid.java
index 6df85903c20a..5381cd77227b 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedUuid.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedUuid.java
@@ -19,14 +19,26 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
-import java.util.UUID;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AutoGeneratedUuidTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
/**
- * Denotes this attribute as recording the auto generated UUID string for the record. Every time a record with this
- * attribute is written to the database it will update the attribute with a {@link UUID#randomUUID} string.
+ * Marks an attribute to be automatically populated with a new UUID on every write operation.
+ *
+ *
Intended Usage:
+ * This annotation is generic and may be applied to any {@link String} attribute, not only key attributes.
+ *
+ *
Generation Semantics:
+ * On every write (put, update, batch write, or transaction write), the attribute is replaced with a new value generated using
+ * {@link java.util.UUID#randomUUID()}, regardless of any existing value.
+ *
+ * Difference from {@code @DynamoDbAutoGeneratedKey}:
+ * {@code @DynamoDbAutoGeneratedKey} is intended for key attributes and generates UUID only when the value is absent.
+ * {@code @DynamoDbAutoGeneratedUuid} annotation always regenerates the UUID on each write.
+ *
+ * Type Restriction:
+ * This annotation may only be applied to attributes of type {@link String}.
*/
@SdkPublicApi
@Target(ElementType.METHOD)
From bbd03ac31eaa2226330be7a63ebdffa92cb689f8 Mon Sep 17 00:00:00 2001
From: Ana Satirbasa
Date: Mon, 16 Feb 2026 16:28:08 +0200
Subject: [PATCH 10/14] Added tests with composite gsi
---
.../extensions/AutoGeneratedKeyExtension.java | 23 +-
.../AutoGeneratedKeyExtensionTest.java | 663 +++++++++++++++---
2 files changed, 582 insertions(+), 104 deletions(-)
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
index 43a1943b521a..e06bce6cb643 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
@@ -27,13 +27,14 @@
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
-import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
@@ -164,20 +165,12 @@ private static void validateNoAutoGeneratedAnnotationConflict(TableMetadata tabl
private void validateAutoGeneratedKeyPlacement(TableMetadata tableMetadata,
Collection taggedAttributeNames) {
- Set allowedKeyAttributes = allowedKeysCache.computeIfAbsent(tableMetadata, metadata -> {
- Set keyAttributes = new HashSet<>();
- keyAttributes.add(metadata.primaryPartitionKey());
- metadata.primarySortKey().ifPresent(keyAttributes::add);
-
- metadata.indices().stream()
- .map(IndexMetadata::name)
- .forEach(indexName -> {
- keyAttributes.add(metadata.indexPartitionKey(indexName));
- metadata.indexSortKey(indexName).ifPresent(keyAttributes::add);
- });
-
- return keyAttributes;
- });
+ Set allowedKeyAttributes =
+ allowedKeysCache.computeIfAbsent(tableMetadata, metadata ->
+ new HashSet<>(metadata.keyAttributes()
+ .stream()
+ .map(KeyAttributeMetadata::name)
+ .collect(Collectors.toSet())));
taggedAttributeNames.stream()
.filter(attrName -> !allowedKeyAttributes.contains(attrName))
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
index de98bb27c519..514bcea5d225 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
@@ -18,6 +18,8 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.FIRST;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.SECOND;
import java.util.List;
import java.util.stream.Collectors;
@@ -34,8 +36,10 @@
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute;
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase;
import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.Order;
import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey;
@@ -46,8 +50,8 @@
public class AutoGeneratedKeyExtensionTest extends LocalDynamoDbSyncTestBase {
- private static final TableSchema TABLE_SCHEMA =
- TableSchema.fromClass(AutogeneratedKeyRecord.class);
+ private static final TableSchema TABLE_SCHEMA =
+ TableSchema.fromClass(BeanWithAutogeneratedKey.class);
private final DynamoDbEnhancedClient enhancedClient =
DynamoDbEnhancedClient.builder()
@@ -58,7 +62,7 @@ public class AutoGeneratedKeyExtensionTest extends LocalDynamoDbSyncTestBase {
.collect(Collectors.toList()))
.build();
- private final DynamoDbTable mappedTable =
+ private final DynamoDbTable mappedTable =
enhancedClient.table(getConcreteTableName("autogenerated-key-table"), TABLE_SCHEMA);
@Before
@@ -73,11 +77,11 @@ public void deleteTable() {
@Test
public void putItem_whenKeysNotPopulated_generatesNewUuids() {
- AutogeneratedKeyRecord record = new AutogeneratedKeyRecord();
+ BeanWithAutogeneratedKey record = new BeanWithAutogeneratedKey();
mappedTable.putItem(record);
- AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
+ BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
isValidUuid(result.getId());
isValidUuid(result.getSortKey());
@@ -87,18 +91,14 @@ public void putItem_whenKeysNotPopulated_generatesNewUuids() {
@Test
public void putItem_whenKeysAlreadyPopulated_preservesExistingUuids() {
- AutogeneratedKeyRecord record = new AutogeneratedKeyRecord();
- record.setId("existing-id");
- record.setSortKey("existing-sk");
- record.setGsiPk("existing-gsiPk");
- record.setGsiSk("existing-gsiSk");
+ BeanWithAutogeneratedKey record = buildBeanWithAutogeneratedKeyAndKeysPopulated();
mappedTable.putItem(record);
- AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
+ BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
assertThat(result.getId()).isEqualTo("existing-id");
- assertThat(result.getSortKey()).isEqualTo("existing-sk");
+ assertThat(result.getSortKey()).isEqualTo("existing-sortKey");
assertThat(result.getGsiPk()).isEqualTo("existing-gsiPk");
assertThat(result.getGsiSk()).isEqualTo("existing-gsiSk");
}
@@ -106,10 +106,10 @@ public void putItem_whenKeysAlreadyPopulated_preservesExistingUuids() {
@Test
public void updateItem_respectsUpdateBehavior() {
// put initial item
- AutogeneratedKeyRecord record = new AutogeneratedKeyRecord();
+ BeanWithAutogeneratedKey record = new BeanWithAutogeneratedKey();
mappedTable.putItem(record);
- AutogeneratedKeyRecord afterPut = mappedTable.scan().items().stream().findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
+ BeanWithAutogeneratedKey afterPut = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
String originalPk = afterPut.getId();
String originalSk = afterPut.getSortKey();
String originalGsiPk = afterPut.getGsiPk();
@@ -117,38 +117,38 @@ public void updateItem_respectsUpdateBehavior() {
// update item
- AutogeneratedKeyRecord update = new AutogeneratedKeyRecord();
+ BeanWithAutogeneratedKey update = new BeanWithAutogeneratedKey();
update.setId(afterPut.getId());
update.setSortKey(afterPut.getSortKey());
mappedTable.updateItem(update);
- AutogeneratedKeyRecord afterUpdate =
+ BeanWithAutogeneratedKey afterUpdate =
mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSortKey())));
// id and sortKey preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys
assertThat(afterUpdate.getId()).isEqualTo(originalPk);
assertThat(afterUpdate.getSortKey()).isEqualTo(originalSk);
- // gsiPk has WRITE_ALWAYS: regenerates UUID on every update
+ // gsiPk has WRITE_ALWAYS -> regenerates UUID on every update
isValidUuid(afterUpdate.getGsiPk());
assertThat(afterUpdate.getGsiPk()).isNotEqualTo(originalGsiPk);
- // gsiSk has WRITE_IF_NOT_EXISTS: preserves original UUID, only writes if null
+ // gsiSk has WRITE_IF_NOT_EXISTS -> preserves original UUID, only writes if null
assertThat(afterUpdate.getGsiSk()).isEqualTo(originalGsiSk);
}
@Test
public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
- AutogeneratedKeyRecord firstRecord = new AutogeneratedKeyRecord();
- AutogeneratedKeyRecord secondRecord = new AutogeneratedKeyRecord();
+ BeanWithAutogeneratedKey firstRecord = new BeanWithAutogeneratedKey();
+ BeanWithAutogeneratedKey secondRecord = new BeanWithAutogeneratedKey();
enhancedClient.batchWriteItem(req -> req.addWriteBatch(
- WriteBatch.builder(AutogeneratedKeyRecord.class)
+ WriteBatch.builder(BeanWithAutogeneratedKey.class)
.mappedTableResource(mappedTable)
.addPutItem(firstRecord)
.addPutItem(secondRecord)
.build()));
- List results = mappedTable.scan().items().stream().collect(Collectors.toList());
+ List results = mappedTable.scan().items().stream().collect(Collectors.toList());
assertThat(results.size()).isEqualTo(2);
isValidUuid(results.get(0).getId());
@@ -159,51 +159,42 @@ public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
@Test
public void batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids() {
- AutogeneratedKeyRecord firstRecord = new AutogeneratedKeyRecord();
- firstRecord.setId("existing-id-1");
- firstRecord.setSortKey("existing-sk-1");
- firstRecord.setGsiPk("existing-gsiPk-1");
- firstRecord.setGsiSk("existing-gsiSk-1");
-
- AutogeneratedKeyRecord secondRecord = new AutogeneratedKeyRecord();
- secondRecord.setId("existing-id-2");
- secondRecord.setSortKey("existing-sk-2");
- secondRecord.setGsiPk("existing-gsiPk-2");
- secondRecord.setGsiSk("existing-gsiSk-2");
+ BeanWithAutogeneratedKey firstRecord = buildBeanWithAutogeneratedKeyAndKeysPopulated(1);
+ BeanWithAutogeneratedKey secondRecord = buildBeanWithAutogeneratedKeyAndKeysPopulated(2);
enhancedClient.batchWriteItem(req -> req.addWriteBatch(
- WriteBatch.builder(AutogeneratedKeyRecord.class)
+ WriteBatch.builder(BeanWithAutogeneratedKey.class)
.mappedTableResource(mappedTable)
.addPutItem(firstRecord)
.addPutItem(secondRecord)
.build()));
- AutogeneratedKeyRecord savedRecord1 =
- mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id-1").sortValue("existing-sk-1")));
- assertThat(savedRecord1.getId()).isEqualTo("existing-id-1");
- assertThat(savedRecord1.getSortKey()).isEqualTo("existing-sk-1");
- assertThat(savedRecord1.getGsiPk()).isEqualTo("existing-gsiPk-1");
- assertThat(savedRecord1.getGsiSk()).isEqualTo("existing-gsiSk-1");
-
- AutogeneratedKeyRecord savedRecord2 =
- mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id-2").sortValue("existing-sk-2")));
- assertThat(savedRecord2.getId()).isEqualTo("existing-id-2");
- assertThat(savedRecord2.getSortKey()).isEqualTo("existing-sk-2");
- assertThat(savedRecord2.getGsiPk()).isEqualTo("existing-gsiPk-2");
- assertThat(savedRecord2.getGsiSk()).isEqualTo("existing-gsiSk-2");
+ BeanWithAutogeneratedKey savedRecord1 =
+ mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_1").sortValue("existing-sortKey_1")));
+ assertThat(savedRecord1.getId()).isEqualTo("existing-id_1");
+ assertThat(savedRecord1.getSortKey()).isEqualTo("existing-sortKey_1");
+ assertThat(savedRecord1.getGsiPk()).isEqualTo("existing-gsiPk_1");
+ assertThat(savedRecord1.getGsiSk()).isEqualTo("existing-gsiSk_1");
+
+ BeanWithAutogeneratedKey savedRecord2 =
+ mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_2").sortValue("existing-sortKey_2")));
+ assertThat(savedRecord2.getId()).isEqualTo("existing-id_2");
+ assertThat(savedRecord2.getSortKey()).isEqualTo("existing-sortKey_2");
+ assertThat(savedRecord2.getGsiPk()).isEqualTo("existing-gsiPk_2");
+ assertThat(savedRecord2.getGsiSk()).isEqualTo("existing-gsiSk_2");
}
@Test
public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
- AutogeneratedKeyRecord record = new AutogeneratedKeyRecord();
+ BeanWithAutogeneratedKey record = new BeanWithAutogeneratedKey();
enhancedClient.transactWriteItems(
TransactWriteItemsEnhancedRequest.builder()
.addPutItem(mappedTable, record)
.build());
- AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
+ BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
isValidUuid(result.getId());
isValidUuid(result.getSortKey());
@@ -213,48 +204,44 @@ public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
@Test
public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() {
- AutogeneratedKeyRecord record = new AutogeneratedKeyRecord();
- record.setId("existing-id-1");
- record.setSortKey("existing-sk-1");
- record.setGsiPk("existing-gsiPk-1");
- record.setGsiSk("existing-gsiSk-1");
+ BeanWithAutogeneratedKey record = buildBeanWithAutogeneratedKeyAndKeysPopulated();
enhancedClient.transactWriteItems(
TransactWriteItemsEnhancedRequest.builder()
.addPutItem(mappedTable, record)
.build());
- AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
+ BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
- assertThat(result.getId()).isEqualTo("existing-id-1");
- assertThat(result.getSortKey()).isEqualTo("existing-sk-1");
- assertThat(result.getGsiPk()).isEqualTo("existing-gsiPk-1");
- assertThat(result.getGsiSk()).isEqualTo("existing-gsiSk-1");
+ assertThat(result.getId()).isEqualTo("existing-id");
+ assertThat(result.getSortKey()).isEqualTo("existing-sortKey");
+ assertThat(result.getGsiPk()).isEqualTo("existing-gsiPk");
+ assertThat(result.getGsiSk()).isEqualTo("existing-gsiSk");
}
@Test
public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() {
String tableName = "versioned-record-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyVersionedRecord.class));
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithAutogeneratedKeyAndVersion.class));
try {
mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- AutogeneratedKeyVersionedRecord record = new AutogeneratedKeyVersionedRecord();
+ BeanWithAutogeneratedKeyAndVersion record = new BeanWithAutogeneratedKeyAndVersion();
record.setId("id");
record.setData("data-v1");
mappedTable.putItem(record);
- AutogeneratedKeyVersionedRecord retrieved = mappedTable.scan().items().stream().findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
+ BeanWithAutogeneratedKeyAndVersion retrieved = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
isValidUuid(retrieved.getId());
assertThat(retrieved.getData()).isEqualTo("data-v1");
assertThat(retrieved.getVersion()).isEqualTo(1L);
retrieved.setData("data-v2");
- AutogeneratedKeyVersionedRecord updated = mappedTable.updateItem(retrieved);
+ BeanWithAutogeneratedKeyAndVersion updated = mappedTable.updateItem(retrieved);
isValidUuid(updated.getId());
assertThat(updated.getData()).isEqualTo("data-v2");
assertThat(updated.getVersion()).isEqualTo(2L);
@@ -270,13 +257,13 @@ public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() {
@Test
public void putItem_whenAnnotationInConflictWithAutogeneratedUuidAnnotation_throwsException() {
String tableName = "conflicting-annotations-record-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyConflictingRecord.class));
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithConflictingAnnotations.class));
try {
mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyConflictingRecord()))
+ assertThatThrownBy(() -> mappedTable.putItem(new BeanWithConflictingAnnotations()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. "
@@ -292,13 +279,13 @@ public void putItem_whenAnnotationInConflictWithAutogeneratedUuidAnnotation_thro
@Test
public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() {
String tableName = "annotation-on-non-key-attribute-record-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyOnNonKeyAttributeRecord.class));
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithAutogeneratedKeyOnNonKeyAttribute.class));
try {
mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyOnNonKeyAttributeRecord()))
+ assertThatThrownBy(() -> mappedTable.putItem(new BeanWithAutogeneratedKeyOnNonKeyAttribute()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
@@ -315,23 +302,24 @@ public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() {
@Test
public void putItem_whenNoAutogeneratedKeyAnnotationIsPresent_doesNotRegenerateUuids() {
String tableName = "no-annotation-record-autogenerated-key-table";
- DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(BeanRecord.class));
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithoutAutogeneratedKey.class));
try {
mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- BeanRecord record = new BeanRecord();
+ BeanWithoutAutogeneratedKey record = new BeanWithoutAutogeneratedKey();
record.setId("existing-id");
- record.setSortKey("existing-sk");
+ record.setSortKey("existing-sortKey");
record.setGsiPk("existing-gsiPk");
record.setGsiSk("existing-gsiSk");
record.setData("test");
mappedTable.putItem(record);
- BeanRecord retrieved = mappedTable.getItem(
- r -> r.key(k -> k.partitionValue("existing-id").sortValue("existing-sk")));
+ BeanWithoutAutogeneratedKey retrieved = mappedTable.getItem(
+ r -> r.key(k -> k.partitionValue("existing-id").sortValue("existing-sortKey")));
assertThat(retrieved.getId()).isEqualTo("existing-id");
- assertThat(retrieved.getSortKey()).isEqualTo("existing-sk");
+ assertThat(retrieved.getSortKey()).isEqualTo("existing-sortKey");
assertThat(retrieved.getGsiPk()).isEqualTo("existing-gsiPk");
assertThat(retrieved.getGsiSk()).isEqualTo("existing-gsiSk");
assertThat(retrieved.getData()).isEqualTo("test");
@@ -345,15 +333,376 @@ public void putItem_whenNoAutogeneratedKeyAnnotationIsPresent_doesNotRegenerateU
@Test
public void createBean_givenAutogeneratedKeyAnnotationAppliedOnNonStringAttributeType_throwsException() {
- assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedKeyInvalidTypeRecord.class))
+ assertThatThrownBy(() -> TableSchema.fromBean(BeanWithAutogeneratedKeyOnAttributeWithInvalidType.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Attribute 'id' of Class type class java.lang.Integer is not a suitable Java Class type to be used "
+ "as a Auto Generated Key attribute. Only String Class type is supported.");
}
+
+ // Tests with Mixed Composite Gsi
+ @Test
+ public void putItem_onBeanWithCompositeGsi_whenKeysNotPopulated_generatesNewUuids() {
+ String tableName = "mixed-gsi-autogenerated-key-table";
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
+
+ try {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+
+ BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
+ mappedTable.putItem(record);
+
+ BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream()
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ isValidUuid(result.getId());
+ isValidUuid(result.getSort());
+ isValidUuid(result.getRootPartitionKey1());
+ isValidUuid(result.getRootPartitionKey2());
+ isValidUuid(result.getRootSortKey1());
+ isValidUuid(result.getRootSortKey2());
+ isValidUuid(result.getFlattenedKeys().flattenedPartitionKey1);
+ isValidUuid(result.getFlattenedKeys().flattenedPartitionKey2);
+ isValidUuid(result.getFlattenedKeys().flattenedSortKey1);
+ isValidUuid(result.getFlattenedKeys().flattenedSortKey2);
+
+ } finally {
+ try {
+ mappedTable.deleteTable();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ @Test
+ public void putItem_onBeanWithCompositeGsi_whenKeysAlreadyPopulated_preservesExistingUuids() {
+ String tableName = "mixed-gsi-autogenerated-key-table";
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
+
+ try {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+ BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated();
+ mappedTable.putItem(record);
+
+ BeanWithMixedCompositeGsi result =
+ mappedTable.scan().items().stream()
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+
+ assertThat(result.getId()).isEqualTo("existing-id");
+ assertThat(result.getSort()).isEqualTo("existing-sort");
+ assertThat(result.getRootPartitionKey1()).isEqualTo("existing-rootPk1");
+ assertThat(result.getRootPartitionKey2()).isEqualTo("existing-rootPk2");
+ assertThat(result.getRootSortKey1()).isEqualTo("existing-rootSk1");
+ assertThat(result.getRootSortKey2()).isEqualTo("existing-rootSk2");
+ assertThat(result.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1");
+ assertThat(result.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2");
+ assertThat(result.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1");
+ assertThat(result.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2");
+
+ } finally {
+ try {
+ mappedTable.deleteTable();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ @Test
+ public void updateItem_onBeanWithCompositeKeys_respectsUpdateBehavior() {
+ String tableName = "mixed-gsi-autogenerated-key-table";
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
+
+ try {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+ BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated();
+ mappedTable.putItem(record);
+
+ // put initial item
+ BeanWithMixedCompositeGsi afterPut =
+ mappedTable.scan().items().stream()
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ String originalPk = afterPut.getId();
+ String originalSk = afterPut.getSort();
+ String originalRootPartitionKey1 = afterPut.getRootPartitionKey1();
+ String originalRootPartitionKey2 = afterPut.getRootPartitionKey2();
+ String originalRootSortKey1 = afterPut.getRootSortKey1();
+ String originalRootSortKey2 = afterPut.getRootSortKey2();
+ String originalFlattenedPartitionKey1 = afterPut.getFlattenedKeys().flattenedPartitionKey1;
+ String originalFlattenedPartitionKey2 = afterPut.getFlattenedKeys().flattenedPartitionKey2;
+ String originalFlattenedSortKey1 = afterPut.getFlattenedKeys().flattenedSortKey1;
+ String originalFlattenedSortKey2 = afterPut.getFlattenedKeys().flattenedSortKey2;
+
+ // update item
+ BeanWithMixedCompositeGsi update = new BeanWithMixedCompositeGsi();
+ update.setId(afterPut.getId());
+ update.setSort(afterPut.getSort());
+
+ mappedTable.updateItem(update);
+ BeanWithMixedCompositeGsi afterUpdate =
+ mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSort())));
+
+
+ // id and sort preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys
+ assertThat(afterUpdate.getId()).isEqualTo(originalPk);
+ assertThat(afterUpdate.getSort()).isEqualTo(originalSk);
+
+ // rootPartitionKey1, rootSortKey1, flattenedPartitionKey1, flattenedSortKey1 have WRITE_ALWAYS
+ // -> regenerates UUID on every update
+ isValidUuid(afterUpdate.getRootPartitionKey1());
+ isValidUuid(afterUpdate.getRootSortKey1());
+ assertThat(afterUpdate.getRootPartitionKey1()).isNotEqualTo(originalRootPartitionKey1);
+ assertThat(afterUpdate.getRootSortKey1()).isNotEqualTo(originalRootSortKey1);
+
+ isValidUuid(afterUpdate.getFlattenedKeys().flattenedPartitionKey1);
+ isValidUuid(afterUpdate.getFlattenedKeys().flattenedSortKey1);
+ assertThat(afterUpdate.getFlattenedKeys().flattenedPartitionKey1).isNotEqualTo(originalFlattenedPartitionKey1);
+ assertThat(afterUpdate.getFlattenedKeys().flattenedSortKey1).isNotEqualTo(originalFlattenedSortKey1);
+
+
+ // rootPartitionKey2, rootSortKey2, flattenedPartitionKey2, flattenedSortKey2 have WRITE_IF_NOT_EXISTS
+ // -> preserves original UUID, only writes if null
+ assertThat(afterUpdate.getRootPartitionKey2()).isEqualTo(originalRootPartitionKey2);
+ assertThat(afterUpdate.getRootSortKey2()).isEqualTo(originalRootSortKey2);
+ assertThat(afterUpdate.getFlattenedKeys().getFlattenedPartitionKey2()).isEqualTo(originalFlattenedPartitionKey2);
+ assertThat(afterUpdate.getFlattenedKeys().getFlattenedSortKey2()).isEqualTo(originalFlattenedSortKey2);
+
+ } finally {
+ try {
+ mappedTable.deleteTable();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ @Test
+ public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() {
+ String tableName = "mixed-gsi-autogenerated-key-table";
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
+
+ try {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+ BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1);
+ BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2);
+
+ enhancedClient.batchWriteItem(req -> req.addWriteBatch(
+ WriteBatch.builder(BeanWithMixedCompositeGsi.class)
+ .mappedTableResource(mappedTable)
+ .addPutItem(firstRecord)
+ .addPutItem(secondRecord)
+ .build()));
+
+ List results = mappedTable.scan().items().stream().collect(Collectors.toList());
+
+ assertThat(results.size()).isEqualTo(2);
+ isValidUuid(results.get(0).getId());
+ isValidUuid(results.get(1).getId());
+ isValidUuid(results.get(0).getSort());
+ isValidUuid(results.get(1).getSort());
+ isValidUuid(results.get(0).getRootPartitionKey1());
+ isValidUuid(results.get(1).getRootPartitionKey1());
+ isValidUuid(results.get(0).getRootPartitionKey2());
+ isValidUuid(results.get(1).getRootPartitionKey2());
+ isValidUuid(results.get(0).getRootSortKey1());
+ isValidUuid(results.get(1).getRootSortKey1());
+ isValidUuid(results.get(0).getRootSortKey2());
+ isValidUuid(results.get(1).getRootSortKey2());
+ isValidUuid(results.get(0).getFlattenedKeys().flattenedPartitionKey1);
+ isValidUuid(results.get(1).getFlattenedKeys().flattenedPartitionKey1);
+ isValidUuid(results.get(0).getFlattenedKeys().flattenedPartitionKey2);
+ isValidUuid(results.get(1).getFlattenedKeys().flattenedPartitionKey2);
+ isValidUuid(results.get(0).getFlattenedKeys().flattenedSortKey1);
+ isValidUuid(results.get(1).getFlattenedKeys().flattenedSortKey1);
+ isValidUuid(results.get(0).getFlattenedKeys().flattenedSortKey2);
+ isValidUuid(results.get(1).getFlattenedKeys().flattenedSortKey2);
+
+ } finally {
+ try {
+ mappedTable.deleteTable();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ @Test
+ public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() {
+ String tableName = "mixed-gsi-autogenerated-key-table";
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
+
+ try {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+ BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1);
+ BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2);
+
+ enhancedClient.batchWriteItem(req -> req.addWriteBatch(WriteBatch.builder(BeanWithMixedCompositeGsi.class)
+ .mappedTableResource(mappedTable)
+ .addPutItem(firstRecord)
+ .addPutItem(secondRecord)
+ .build()));
+
+ BeanWithMixedCompositeGsi firstSavedRecord =
+ mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_1").sortValue("existing-sort_1")));
+ assertThat(firstSavedRecord.getId()).isEqualTo("existing-id_1");
+ assertThat(firstSavedRecord.getSort()).isEqualTo("existing-sort_1");
+ assertThat(firstSavedRecord.getRootPartitionKey1()).isEqualTo("existing-rootPk1_1");
+ assertThat(firstSavedRecord.getRootPartitionKey2()).isEqualTo("existing-rootPk2_1");
+ assertThat(firstSavedRecord.getRootSortKey1()).isEqualTo("existing-rootSk1_1");
+ assertThat(firstSavedRecord.getRootSortKey2()).isEqualTo("existing-rootSk2_1");
+ assertThat(firstSavedRecord.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_1");
+ assertThat(firstSavedRecord.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_1");
+ assertThat(firstSavedRecord.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_1");
+ assertThat(firstSavedRecord.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_1");
+
+
+ BeanWithMixedCompositeGsi secondSavedRecord =
+ mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_2").sortValue("existing-sort_2")));
+
+ assertThat(secondSavedRecord.getId()).isEqualTo("existing-id_2");
+ assertThat(secondSavedRecord.getSort()).isEqualTo("existing-sort_2");
+ assertThat(secondSavedRecord.getRootPartitionKey1()).isEqualTo("existing-rootPk1_2");
+ assertThat(secondSavedRecord.getRootPartitionKey2()).isEqualTo("existing-rootPk2_2");
+ assertThat(secondSavedRecord.getRootSortKey1()).isEqualTo("existing-rootSk1_2");
+ assertThat(secondSavedRecord.getRootSortKey2()).isEqualTo("existing-rootSk2_2");
+
+ assertThat(secondSavedRecord.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_2");
+ assertThat(secondSavedRecord.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_2");
+ assertThat(secondSavedRecord.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_2");
+ assertThat(secondSavedRecord.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_2");
+
+ } finally {
+ try {
+ mappedTable.deleteTable();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ @Test
+ public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() {
+ String tableName = "mixed-gsi-autogenerated-key-table";
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
+
+ try {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+
+ BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
+ enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
+ .addPutItem(mappedTable, record)
+ .build());
+
+ BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ isValidUuid(result.getId());
+ isValidUuid(result.getSort());
+ isValidUuid(result.getRootPartitionKey1());
+ isValidUuid(result.getRootPartitionKey2());
+ isValidUuid(result.getRootSortKey1());
+ isValidUuid(result.getRootSortKey2());
+ isValidUuid(result.getFlattenedKeys().flattenedPartitionKey1);
+ isValidUuid(result.getFlattenedKeys().flattenedPartitionKey2);
+ isValidUuid(result.getFlattenedKeys().flattenedSortKey1);
+ isValidUuid(result.getFlattenedKeys().flattenedSortKey2);
+
+ } finally {
+ try {
+ mappedTable.deleteTable();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ @Test
+ public void transactWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() {
+ String tableName = "mixed-gsi-autogenerated-key-table";
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
+
+ try {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+ BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(1);
+ enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
+ .addPutItem(mappedTable, record)
+ .build());
+
+ BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ assertThat(result.getId()).isEqualTo("existing-id_1");
+ assertThat(result.getSort()).isEqualTo("existing-sort_1");
+ assertThat(result.getRootPartitionKey1()).isEqualTo("existing-rootPk1_1");
+ assertThat(result.getRootPartitionKey2()).isEqualTo("existing-rootPk2_1");
+ assertThat(result.getRootSortKey1()).isEqualTo("existing-rootSk1_1");
+ assertThat(result.getRootSortKey2()).isEqualTo("existing-rootSk2_1");
+ assertThat(result.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_1");
+ assertThat(result.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_1");
+ assertThat(result.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_1");
+ assertThat(result.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_1");
+
+ } finally {
+ try {
+ mappedTable.deleteTable();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+
+ private static BeanWithAutogeneratedKey buildBeanWithAutogeneratedKeyAndKeysPopulated() {
+ return buildBeanWithAutogeneratedKeyAndKeysPopulated(null);
+ }
+
+ private static BeanWithAutogeneratedKey buildBeanWithAutogeneratedKeyAndKeysPopulated(Integer index) {
+ String suffix = index == null ? "" : "_" + index;
+
+ BeanWithAutogeneratedKey record = new BeanWithAutogeneratedKey();
+ record.setId("existing-id" + suffix);
+ record.setSortKey("existing-sortKey" + suffix);
+ record.setGsiPk("existing-gsiPk" + suffix);
+ record.setGsiSk("existing-gsiSk" + suffix);
+
+ return record;
+ }
+
+ private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated() {
+ return buildBeanWithCompositeGsiAndKeysPopulated(null);
+ }
+
+ private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated(Integer index) {
+ String suffix = index == null ? "" : "_" + index;
+
+ BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
+ record.setId("existing-id" + suffix);
+ record.setSort("existing-sort" + suffix);
+ record.setRootPartitionKey1("existing-rootPk1" + suffix);
+ record.setRootPartitionKey2("existing-rootPk2" + suffix);
+ record.setRootSortKey1("existing-rootSk1" + suffix);
+ record.setRootSortKey2("existing-rootSk2" + suffix);
+
+ BeanWithMixedCompositeGsi.FlattenedKeys flattenedKeys = new BeanWithMixedCompositeGsi.FlattenedKeys();
+ flattenedKeys.setFlattenedPartitionKey1("existing-flattenedPk1" + suffix);
+ flattenedKeys.setFlattenedPartitionKey2("existing-flattenedPk2" + suffix);
+ flattenedKeys.setFlattenedSortKey1("existing-flattenedSk1" + suffix);
+ flattenedKeys.setFlattenedSortKey2("existing-flattenedSk2" + suffix);
+ record.setFlattenedKeys(flattenedKeys);
+
+ return record;
+ }
+
+
@DynamoDbBean
- public static class BeanRecord {
+ public static class BeanWithoutAutogeneratedKey {
private String id;
private String sortKey;
private String gsiPk;
@@ -408,7 +757,7 @@ public void setData(String data) {
}
@DynamoDbBean
- public static class AutogeneratedKeyRecord {
+ public static class BeanWithAutogeneratedKey {
private String id;
private String sortKey;
private String gsiPk;
@@ -458,7 +807,7 @@ public void setGsiSk(String gsiSk) {
}
@DynamoDbBean
- public static class AutogeneratedKeyVersionedRecord {
+ public static class BeanWithAutogeneratedKeyAndVersion {
private String id;
private Long version;
private String data;
@@ -492,7 +841,7 @@ public void setData(String data) {
}
@DynamoDbBean
- public static class AutogeneratedKeyInvalidTypeRecord {
+ public static class BeanWithAutogeneratedKeyOnAttributeWithInvalidType {
private Integer id;
@DynamoDbPartitionKey
@@ -507,7 +856,7 @@ public void setId(Integer id) {
}
@DynamoDbBean
- public static class AutogeneratedKeyOnNonKeyAttributeRecord {
+ public static class BeanWithAutogeneratedKeyOnNonKeyAttribute {
private String id;
private String nonKeyAttribute;
@@ -531,7 +880,7 @@ public void setNonKeyAttribute(String nonKeyAttribute) {
}
@DynamoDbBean
- public static class AutogeneratedKeyConflictingRecord {
+ public static class BeanWithConflictingAnnotations {
private String id;
@DynamoDbPartitionKey
@@ -545,6 +894,142 @@ public void setId(String id) {
this.id = id;
}
}
+
+ @DynamoDbBean
+ public static class BeanWithMixedCompositeGsi {
+ private String id;
+ private String sort;
+ private String rootPartitionKey1;
+ private String rootPartitionKey2;
+ private String rootSortKey1;
+ private String rootSortKey2;
+ private FlattenedKeys flattenedKeys;
+
+ @DynamoDbPartitionKey
+ @DynamoDbAutoGeneratedKey
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @DynamoDbSortKey
+ @DynamoDbAutoGeneratedKey
+ public String getSort() {
+ return sort;
+ }
+
+ public void setSort(String sort) {
+ this.sort = sort;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
+ @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = FIRST)
+ public String getRootPartitionKey1() {
+ return rootPartitionKey1;
+ }
+
+ public void setRootPartitionKey1(String rootPartitionKey1) {
+ this.rootPartitionKey1 = rootPartitionKey1;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
+ @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = SECOND)
+ public String getRootPartitionKey2() {
+ return rootPartitionKey2;
+ }
+
+ public void setRootPartitionKey2(String rootPartitionKey2) {
+ this.rootPartitionKey2 = rootPartitionKey2;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
+ @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = FIRST)
+ public String getRootSortKey1() {
+ return rootSortKey1;
+ }
+
+ public void setRootSortKey1(String rootSortKey1) {
+ this.rootSortKey1 = rootSortKey1;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
+ @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = SECOND)
+ public String getRootSortKey2() {
+ return rootSortKey2;
+ }
+
+ public void setRootSortKey2(String rootSortKey2) {
+ this.rootSortKey2 = rootSortKey2;
+ }
+
+ @DynamoDbFlatten
+ public FlattenedKeys getFlattenedKeys() {
+ return flattenedKeys;
+ }
+
+ public void setFlattenedKeys(FlattenedKeys flattenedKeys) {
+ this.flattenedKeys = flattenedKeys;
+ }
+
+ @DynamoDbBean
+ public static class FlattenedKeys {
+ private String flattenedPartitionKey1;
+ private String flattenedPartitionKey2;
+ private String flattenedSortKey1;
+ private String flattenedSortKey2;
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
+ @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.THIRD)
+ public String getFlattenedPartitionKey1() {
+ return flattenedPartitionKey1;
+ }
+
+ public void setFlattenedPartitionKey1(String flattenedPartitionKey1) {
+ this.flattenedPartitionKey1 = flattenedPartitionKey1;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
+ @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.FOURTH)
+ public String getFlattenedPartitionKey2() {
+ return flattenedPartitionKey2;
+ }
+
+ public void setFlattenedPartitionKey2(String flattenedPartitionKey2) {
+ this.flattenedPartitionKey2 = flattenedPartitionKey2;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
+ @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.THIRD)
+ public String getFlattenedSortKey1() {
+ return flattenedSortKey1;
+ }
+
+ public void setFlattenedSortKey1(String flattenedSortKey1) {
+ this.flattenedSortKey1 = flattenedSortKey1;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
+ @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.FOURTH)
+ public String getFlattenedSortKey2() {
+ return flattenedSortKey2;
+ }
+
+ public void setFlattenedSortKey2(String flattenedSortKey2) {
+ this.flattenedSortKey2 = flattenedSortKey2;
+ }
+ }
+ }
}
From e654ef5c681d6aadb6af51909c6cacd250c8cad9 Mon Sep 17 00:00:00 2001
From: Ana Satirbasa
Date: Mon, 16 Feb 2026 19:09:56 +0200
Subject: [PATCH 11/14] Added tests with composite gsi
---
.../extensions/AutoGeneratedKeyExtension.java | 27 +-
.../annotations/DynamoDbAutoGeneratedKey.java | 2 -
.../AutoGeneratedKeyCompositeGsiTest.java | 413 +++++++++++++++
.../AutoGeneratedKeyExtensionTest.java | 488 +-----------------
4 files changed, 433 insertions(+), 497 deletions(-)
create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
index e06bce6cb643..4c3ec6e5aa27 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
@@ -27,14 +27,13 @@
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
-import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
@@ -165,12 +164,19 @@ private static void validateNoAutoGeneratedAnnotationConflict(TableMetadata tabl
private void validateAutoGeneratedKeyPlacement(TableMetadata tableMetadata,
Collection taggedAttributeNames) {
- Set allowedKeyAttributes =
- allowedKeysCache.computeIfAbsent(tableMetadata, metadata ->
- new HashSet<>(metadata.keyAttributes()
- .stream()
- .map(KeyAttributeMetadata::name)
- .collect(Collectors.toSet())));
+ Set allowedKeyAttributes = allowedKeysCache.computeIfAbsent(tableMetadata, metadata -> {
+
+ // Add primary keys
+ Set keyAttributes = new HashSet<>(metadata.primaryKeys());
+
+ // Add all secondary index keys
+ metadata.indices().stream().map(IndexMetadata::name).forEach(indexName -> {
+ keyAttributes.addAll(metadata.indexPartitionKeys(indexName));
+ keyAttributes.addAll(metadata.indexSortKeys(indexName));
+ });
+
+ return keyAttributes;
+ });
taggedAttributeNames.stream()
.filter(attrName -> !allowedKeyAttributes.contains(attrName))
@@ -241,9 +247,8 @@ public void validateType(String attributeName,
@Override
public Consumer modifyMetadata(String attributeName,
AttributeValueType attributeValueType) {
- // Records attribute names annotated with @DynamoDbAutoGeneratedKey for lookup during write processing.
- return metadata -> metadata.addCustomMetadataObject(
- CUSTOM_METADATA_KEY, Collections.singleton(attributeName));
+ return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, Collections.singleton(attributeName))
+ .markAttributeAsKey(attributeName, attributeValueType);
}
}
}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java
index cdad8d30616b..302a72df76c4 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java
@@ -22,9 +22,7 @@
import java.lang.annotation.Target;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AutoGeneratedKeyTag;
-import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
-import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
/**
* Marks a key attribute to be automatically populated with a UUID when the value is null or empty during a write operation.
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
new file mode 100644
index 000000000000..63477ecaa9f9
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
@@ -0,0 +1,413 @@
+/*
+ * 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.extensions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.FIRST;
+import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.SECOND;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+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.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey;
+import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase;
+import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.Order;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
+import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest;
+import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch;
+
+public class AutoGeneratedKeyCompositeGsiTest extends LocalDynamoDbSyncTestBase {
+
+ private static final TableSchema TABLE_SCHEMA =
+ TableSchema.fromClass(BeanWithMixedCompositeGsi.class);
+
+ private final DynamoDbEnhancedClient enhancedClient =
+ DynamoDbEnhancedClient.builder()
+ .dynamoDbClient(getDynamoDbClient())
+ .extensions(Stream.concat(
+ ExtensionResolver.defaultExtensions().stream(),
+ Stream.of(AutoGeneratedKeyExtension.create()))
+ .collect(Collectors.toList()))
+ .build();
+
+ private final DynamoDbTable mappedTable =
+ enhancedClient.table(getConcreteTableName("mixed-gsi-autogenerated-key-table"), TABLE_SCHEMA);
+
+ @Before
+ public void createTable() {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+ }
+
+ @After
+ public void deleteTable() {
+ getDynamoDbClient().deleteTable(r -> r.tableName(
+ getConcreteTableName("mixed-gsi-autogenerated-key-table")));
+ }
+
+ @Test
+ public void putItem_whenKeysNotPopulated_generatesNewUuids() {
+ BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
+
+ mappedTable.putItem(record);
+ BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream()
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ assertAllKeysAreValidUuids(result);
+ }
+
+ @Test
+ public void putItem_whenKeysAlreadyPopulated_preservesExistingUuids() {
+ BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated();
+ mappedTable.putItem(record);
+
+ BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream()
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ assertCompositeKeyValuesArePreserved(result);
+ }
+
+ @Test
+ public void updateItem_onBeanWithCompositeKeys_respectsUpdateBehavior() {
+ BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated();
+
+ // put item with all keys populated
+ mappedTable.putItem(record);
+ BeanWithMixedCompositeGsi afterPut =
+ mappedTable.scan().items().stream()
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ String originalPk = afterPut.getId();
+ String originalSk = afterPut.getSort();
+ String originalRootPartitionKey1 = afterPut.getRootPartitionKey1();
+ String originalRootPartitionKey2 = afterPut.getRootPartitionKey2();
+ String originalRootSortKey1 = afterPut.getRootSortKey1();
+ String originalRootSortKey2 = afterPut.getRootSortKey2();
+ String originalFlattenedPartitionKey1 = afterPut.getFlattenedKeys().flattenedPartitionKey1;
+ String originalFlattenedPartitionKey2 = afterPut.getFlattenedKeys().flattenedPartitionKey2;
+ String originalFlattenedSortKey1 = afterPut.getFlattenedKeys().flattenedSortKey1;
+ String originalFlattenedSortKey2 = afterPut.getFlattenedKeys().flattenedSortKey2;
+
+ // update item
+ BeanWithMixedCompositeGsi update = new BeanWithMixedCompositeGsi();
+ update.setId(afterPut.getId());
+ update.setSort(afterPut.getSort());
+
+ mappedTable.updateItem(update);
+ BeanWithMixedCompositeGsi afterUpdate =
+ mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSort())));
+
+
+ assertAllKeysAreValidUuids(afterUpdate);
+
+ // id and sort preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys
+ assertThat(afterUpdate.getId()).isEqualTo(originalPk);
+ assertThat(afterUpdate.getSort()).isEqualTo(originalSk);
+
+ // rootPartitionKey1, rootSortKey1, flattenedPartitionKey1, flattenedSortKey1 have WRITE_ALWAYS
+ // -> regenerates UUID on every update
+ assertThat(afterUpdate.getRootPartitionKey1()).isNotEqualTo(originalRootPartitionKey1);
+ assertThat(afterUpdate.getRootSortKey1()).isNotEqualTo(originalRootSortKey1);
+ assertThat(afterUpdate.getFlattenedKeys().flattenedPartitionKey1).isNotEqualTo(originalFlattenedPartitionKey1);
+ assertThat(afterUpdate.getFlattenedKeys().flattenedSortKey1).isNotEqualTo(originalFlattenedSortKey1);
+
+ // rootPartitionKey2, rootSortKey2, flattenedPartitionKey2, flattenedSortKey2 have WRITE_IF_NOT_EXISTS
+ // -> preserves original UUID, only writes if null
+ assertThat(afterUpdate.getRootPartitionKey2()).isEqualTo(originalRootPartitionKey2);
+ assertThat(afterUpdate.getRootSortKey2()).isEqualTo(originalRootSortKey2);
+ assertThat(afterUpdate.getFlattenedKeys().getFlattenedPartitionKey2()).isEqualTo(originalFlattenedPartitionKey2);
+ assertThat(afterUpdate.getFlattenedKeys().getFlattenedSortKey2()).isEqualTo(originalFlattenedSortKey2);
+ }
+
+ @Test
+ public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() {
+ BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1);
+ BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2);
+
+ enhancedClient.batchWriteItem(req -> req.addWriteBatch(
+ WriteBatch.builder(BeanWithMixedCompositeGsi.class)
+ .mappedTableResource(mappedTable)
+ .addPutItem(firstRecord)
+ .addPutItem(secondRecord)
+ .build()));
+
+ List results = mappedTable.scan().items().stream().collect(Collectors.toList());
+
+ assertThat(results.size()).isEqualTo(2);
+ assertAllKeysAreValidUuids(results.get(0));
+ assertAllKeysAreValidUuids(results.get(1));
+ }
+
+ @Test
+ public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() {
+ BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1);
+ BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2);
+
+ enhancedClient.batchWriteItem(req -> req.addWriteBatch(WriteBatch.builder(BeanWithMixedCompositeGsi.class)
+ .mappedTableResource(mappedTable)
+ .addPutItem(firstRecord)
+ .addPutItem(secondRecord)
+ .build()));
+
+ BeanWithMixedCompositeGsi firstSavedRecord =
+ mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_1").sortValue("existing-sort_1")));
+ assertCompositeKeyValuesArePreserved(firstSavedRecord, 1);
+
+ BeanWithMixedCompositeGsi secondSavedRecord =
+ mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_2").sortValue("existing-sort_2")));
+ assertCompositeKeyValuesArePreserved(secondSavedRecord, 2);
+ }
+
+ @Test
+ public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() {
+ BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
+ enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
+ .addPutItem(mappedTable, record)
+ .build());
+
+ BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ assertAllKeysAreValidUuids(result);
+ }
+
+ @Test
+ public void transactWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() {
+ BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated();
+ enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
+ .addPutItem(mappedTable, record)
+ .build());
+
+ BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ assertCompositeKeyValuesArePreserved(result);
+ }
+
+ private static void assertAllKeysAreValidUuids(BeanWithMixedCompositeGsi record) {
+ isValidUuid(record.getId());
+ isValidUuid(record.getSort());
+ isValidUuid(record.getRootPartitionKey1());
+ isValidUuid(record.getRootPartitionKey2());
+ isValidUuid(record.getRootSortKey1());
+ isValidUuid(record.getRootSortKey2());
+ isValidUuid(record.getFlattenedKeys().flattenedPartitionKey1);
+ isValidUuid(record.getFlattenedKeys().flattenedPartitionKey2);
+ isValidUuid(record.getFlattenedKeys().flattenedSortKey1);
+ isValidUuid(record.getFlattenedKeys().flattenedSortKey2);
+ }
+
+ private static void assertCompositeKeyValuesArePreserved(BeanWithMixedCompositeGsi actual) {
+ assertCompositeKeyValuesArePreserved(actual, null);
+ }
+
+ private static void assertCompositeKeyValuesArePreserved(BeanWithMixedCompositeGsi actual, Integer recordIndex) {
+ String suffix = recordIndex == null ? "" : "_" + recordIndex;
+
+ assertThat(actual.getId()).isEqualTo("existing-id" + suffix);
+ assertThat(actual.getSort()).isEqualTo("existing-sort" + suffix);
+ assertThat(actual.getRootPartitionKey1()).isEqualTo("existing-rootPk1" + suffix);
+ assertThat(actual.getRootPartitionKey2()).isEqualTo("existing-rootPk2" + suffix);
+ assertThat(actual.getRootSortKey1()).isEqualTo("existing-rootSk1" + suffix);
+ assertThat(actual.getRootSortKey2()).isEqualTo("existing-rootSk2" + suffix);
+
+ assertThat(actual.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1" + suffix);
+ assertThat(actual.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2" + suffix);
+ assertThat(actual.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1" + suffix);
+ assertThat(actual.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2" + suffix);
+ }
+
+ private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated() {
+ return buildBeanWithCompositeGsiAndKeysPopulated(null);
+ }
+
+ private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated(Integer index) {
+ String suffix = index == null ? "" : "_" + index;
+
+ BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
+ record.setId("existing-id" + suffix);
+ record.setSort("existing-sort" + suffix);
+ record.setRootPartitionKey1("existing-rootPk1" + suffix);
+ record.setRootPartitionKey2("existing-rootPk2" + suffix);
+ record.setRootSortKey1("existing-rootSk1" + suffix);
+ record.setRootSortKey2("existing-rootSk2" + suffix);
+
+ BeanWithMixedCompositeGsi.FlattenedKeys flattenedKeys = new BeanWithMixedCompositeGsi.FlattenedKeys();
+ flattenedKeys.setFlattenedPartitionKey1("existing-flattenedPk1" + suffix);
+ flattenedKeys.setFlattenedPartitionKey2("existing-flattenedPk2" + suffix);
+ flattenedKeys.setFlattenedSortKey1("existing-flattenedSk1" + suffix);
+ flattenedKeys.setFlattenedSortKey2("existing-flattenedSk2" + suffix);
+ record.setFlattenedKeys(flattenedKeys);
+
+ return record;
+ }
+
+ @DynamoDbBean
+ public static class BeanWithMixedCompositeGsi {
+ private String id;
+ private String sort;
+ private String rootPartitionKey1;
+ private String rootPartitionKey2;
+ private String rootSortKey1;
+ private String rootSortKey2;
+ private FlattenedKeys flattenedKeys;
+
+ @DynamoDbPartitionKey
+ @DynamoDbAutoGeneratedKey
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @DynamoDbSortKey
+ @DynamoDbAutoGeneratedKey
+ public String getSort() {
+ return sort;
+ }
+
+ public void setSort(String sort) {
+ this.sort = sort;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
+ @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = FIRST)
+ public String getRootPartitionKey1() {
+ return rootPartitionKey1;
+ }
+
+ public void setRootPartitionKey1(String rootPartitionKey1) {
+ this.rootPartitionKey1 = rootPartitionKey1;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
+ @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = SECOND)
+ public String getRootPartitionKey2() {
+ return rootPartitionKey2;
+ }
+
+ public void setRootPartitionKey2(String rootPartitionKey2) {
+ this.rootPartitionKey2 = rootPartitionKey2;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
+ @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = FIRST)
+ public String getRootSortKey1() {
+ return rootSortKey1;
+ }
+
+ public void setRootSortKey1(String rootSortKey1) {
+ this.rootSortKey1 = rootSortKey1;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
+ @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = SECOND)
+ public String getRootSortKey2() {
+ return rootSortKey2;
+ }
+
+ public void setRootSortKey2(String rootSortKey2) {
+ this.rootSortKey2 = rootSortKey2;
+ }
+
+ @DynamoDbFlatten
+ public FlattenedKeys getFlattenedKeys() {
+ return flattenedKeys;
+ }
+
+ public void setFlattenedKeys(FlattenedKeys flattenedKeys) {
+ this.flattenedKeys = flattenedKeys;
+ }
+
+ @DynamoDbBean
+ public static class FlattenedKeys {
+ private String flattenedPartitionKey1;
+ private String flattenedPartitionKey2;
+ private String flattenedSortKey1;
+ private String flattenedSortKey2;
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
+ @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.THIRD)
+ public String getFlattenedPartitionKey1() {
+ return flattenedPartitionKey1;
+ }
+
+ public void setFlattenedPartitionKey1(String flattenedPartitionKey1) {
+ this.flattenedPartitionKey1 = flattenedPartitionKey1;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
+ @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.FOURTH)
+ public String getFlattenedPartitionKey2() {
+ return flattenedPartitionKey2;
+ }
+
+ public void setFlattenedPartitionKey2(String flattenedPartitionKey2) {
+ this.flattenedPartitionKey2 = flattenedPartitionKey2;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
+ @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.THIRD)
+ public String getFlattenedSortKey1() {
+ return flattenedSortKey1;
+ }
+
+ public void setFlattenedSortKey1(String flattenedSortKey1) {
+ this.flattenedSortKey1 = flattenedSortKey1;
+ }
+
+ @DynamoDbAutoGeneratedKey
+ @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
+ @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.FOURTH)
+ public String getFlattenedSortKey2() {
+ return flattenedSortKey2;
+ }
+
+ public void setFlattenedSortKey2(String flattenedSortKey2) {
+ this.flattenedSortKey2 = flattenedSortKey2;
+ }
+ }
+ }
+}
+
+
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
index 514bcea5d225..405f87daad95 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
@@ -18,8 +18,6 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid;
-import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.FIRST;
-import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.SECOND;
import java.util.List;
import java.util.stream.Collectors;
@@ -36,10 +34,8 @@
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute;
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase;
import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver;
-import software.amazon.awssdk.enhanced.dynamodb.mapper.Order;
import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
-import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey;
@@ -285,7 +281,10 @@ public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() {
try {
mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- assertThatThrownBy(() -> mappedTable.putItem(new BeanWithAutogeneratedKeyOnNonKeyAttribute()))
+ BeanWithAutogeneratedKeyOnNonKeyAttribute bean = new BeanWithAutogeneratedKeyOnNonKeyAttribute();
+ bean.setId("id");
+
+ assertThatThrownBy(() -> mappedTable.putItem(bean))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"@DynamoDbAutoGeneratedKey can only be applied to key attributes: "
@@ -341,324 +340,6 @@ public void createBean_givenAutogeneratedKeyAnnotationAppliedOnNonStringAttribut
}
- // Tests with Mixed Composite Gsi
- @Test
- public void putItem_onBeanWithCompositeGsi_whenKeysNotPopulated_generatesNewUuids() {
- String tableName = "mixed-gsi-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
-
- try {
- mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
-
- BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
- mappedTable.putItem(record);
-
- BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream()
- .findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
-
- isValidUuid(result.getId());
- isValidUuid(result.getSort());
- isValidUuid(result.getRootPartitionKey1());
- isValidUuid(result.getRootPartitionKey2());
- isValidUuid(result.getRootSortKey1());
- isValidUuid(result.getRootSortKey2());
- isValidUuid(result.getFlattenedKeys().flattenedPartitionKey1);
- isValidUuid(result.getFlattenedKeys().flattenedPartitionKey2);
- isValidUuid(result.getFlattenedKeys().flattenedSortKey1);
- isValidUuid(result.getFlattenedKeys().flattenedSortKey2);
-
- } finally {
- try {
- mappedTable.deleteTable();
- } catch (Exception ignored) {
- }
- }
- }
-
- @Test
- public void putItem_onBeanWithCompositeGsi_whenKeysAlreadyPopulated_preservesExistingUuids() {
- String tableName = "mixed-gsi-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
-
- try {
- mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated();
- mappedTable.putItem(record);
-
- BeanWithMixedCompositeGsi result =
- mappedTable.scan().items().stream()
- .findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
-
-
- assertThat(result.getId()).isEqualTo("existing-id");
- assertThat(result.getSort()).isEqualTo("existing-sort");
- assertThat(result.getRootPartitionKey1()).isEqualTo("existing-rootPk1");
- assertThat(result.getRootPartitionKey2()).isEqualTo("existing-rootPk2");
- assertThat(result.getRootSortKey1()).isEqualTo("existing-rootSk1");
- assertThat(result.getRootSortKey2()).isEqualTo("existing-rootSk2");
- assertThat(result.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1");
- assertThat(result.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2");
- assertThat(result.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1");
- assertThat(result.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2");
-
- } finally {
- try {
- mappedTable.deleteTable();
- } catch (Exception ignored) {
- }
- }
- }
-
- @Test
- public void updateItem_onBeanWithCompositeKeys_respectsUpdateBehavior() {
- String tableName = "mixed-gsi-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
-
- try {
- mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated();
- mappedTable.putItem(record);
-
- // put initial item
- BeanWithMixedCompositeGsi afterPut =
- mappedTable.scan().items().stream()
- .findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
-
- String originalPk = afterPut.getId();
- String originalSk = afterPut.getSort();
- String originalRootPartitionKey1 = afterPut.getRootPartitionKey1();
- String originalRootPartitionKey2 = afterPut.getRootPartitionKey2();
- String originalRootSortKey1 = afterPut.getRootSortKey1();
- String originalRootSortKey2 = afterPut.getRootSortKey2();
- String originalFlattenedPartitionKey1 = afterPut.getFlattenedKeys().flattenedPartitionKey1;
- String originalFlattenedPartitionKey2 = afterPut.getFlattenedKeys().flattenedPartitionKey2;
- String originalFlattenedSortKey1 = afterPut.getFlattenedKeys().flattenedSortKey1;
- String originalFlattenedSortKey2 = afterPut.getFlattenedKeys().flattenedSortKey2;
-
- // update item
- BeanWithMixedCompositeGsi update = new BeanWithMixedCompositeGsi();
- update.setId(afterPut.getId());
- update.setSort(afterPut.getSort());
-
- mappedTable.updateItem(update);
- BeanWithMixedCompositeGsi afterUpdate =
- mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSort())));
-
-
- // id and sort preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys
- assertThat(afterUpdate.getId()).isEqualTo(originalPk);
- assertThat(afterUpdate.getSort()).isEqualTo(originalSk);
-
- // rootPartitionKey1, rootSortKey1, flattenedPartitionKey1, flattenedSortKey1 have WRITE_ALWAYS
- // -> regenerates UUID on every update
- isValidUuid(afterUpdate.getRootPartitionKey1());
- isValidUuid(afterUpdate.getRootSortKey1());
- assertThat(afterUpdate.getRootPartitionKey1()).isNotEqualTo(originalRootPartitionKey1);
- assertThat(afterUpdate.getRootSortKey1()).isNotEqualTo(originalRootSortKey1);
-
- isValidUuid(afterUpdate.getFlattenedKeys().flattenedPartitionKey1);
- isValidUuid(afterUpdate.getFlattenedKeys().flattenedSortKey1);
- assertThat(afterUpdate.getFlattenedKeys().flattenedPartitionKey1).isNotEqualTo(originalFlattenedPartitionKey1);
- assertThat(afterUpdate.getFlattenedKeys().flattenedSortKey1).isNotEqualTo(originalFlattenedSortKey1);
-
-
- // rootPartitionKey2, rootSortKey2, flattenedPartitionKey2, flattenedSortKey2 have WRITE_IF_NOT_EXISTS
- // -> preserves original UUID, only writes if null
- assertThat(afterUpdate.getRootPartitionKey2()).isEqualTo(originalRootPartitionKey2);
- assertThat(afterUpdate.getRootSortKey2()).isEqualTo(originalRootSortKey2);
- assertThat(afterUpdate.getFlattenedKeys().getFlattenedPartitionKey2()).isEqualTo(originalFlattenedPartitionKey2);
- assertThat(afterUpdate.getFlattenedKeys().getFlattenedSortKey2()).isEqualTo(originalFlattenedSortKey2);
-
- } finally {
- try {
- mappedTable.deleteTable();
- } catch (Exception ignored) {
- }
- }
- }
-
- @Test
- public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() {
- String tableName = "mixed-gsi-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
-
- try {
- mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1);
- BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2);
-
- enhancedClient.batchWriteItem(req -> req.addWriteBatch(
- WriteBatch.builder(BeanWithMixedCompositeGsi.class)
- .mappedTableResource(mappedTable)
- .addPutItem(firstRecord)
- .addPutItem(secondRecord)
- .build()));
-
- List results = mappedTable.scan().items().stream().collect(Collectors.toList());
-
- assertThat(results.size()).isEqualTo(2);
- isValidUuid(results.get(0).getId());
- isValidUuid(results.get(1).getId());
- isValidUuid(results.get(0).getSort());
- isValidUuid(results.get(1).getSort());
- isValidUuid(results.get(0).getRootPartitionKey1());
- isValidUuid(results.get(1).getRootPartitionKey1());
- isValidUuid(results.get(0).getRootPartitionKey2());
- isValidUuid(results.get(1).getRootPartitionKey2());
- isValidUuid(results.get(0).getRootSortKey1());
- isValidUuid(results.get(1).getRootSortKey1());
- isValidUuid(results.get(0).getRootSortKey2());
- isValidUuid(results.get(1).getRootSortKey2());
- isValidUuid(results.get(0).getFlattenedKeys().flattenedPartitionKey1);
- isValidUuid(results.get(1).getFlattenedKeys().flattenedPartitionKey1);
- isValidUuid(results.get(0).getFlattenedKeys().flattenedPartitionKey2);
- isValidUuid(results.get(1).getFlattenedKeys().flattenedPartitionKey2);
- isValidUuid(results.get(0).getFlattenedKeys().flattenedSortKey1);
- isValidUuid(results.get(1).getFlattenedKeys().flattenedSortKey1);
- isValidUuid(results.get(0).getFlattenedKeys().flattenedSortKey2);
- isValidUuid(results.get(1).getFlattenedKeys().flattenedSortKey2);
-
- } finally {
- try {
- mappedTable.deleteTable();
- } catch (Exception ignored) {
- }
- }
- }
-
- @Test
- public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() {
- String tableName = "mixed-gsi-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
-
- try {
- mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1);
- BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2);
-
- enhancedClient.batchWriteItem(req -> req.addWriteBatch(WriteBatch.builder(BeanWithMixedCompositeGsi.class)
- .mappedTableResource(mappedTable)
- .addPutItem(firstRecord)
- .addPutItem(secondRecord)
- .build()));
-
- BeanWithMixedCompositeGsi firstSavedRecord =
- mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_1").sortValue("existing-sort_1")));
- assertThat(firstSavedRecord.getId()).isEqualTo("existing-id_1");
- assertThat(firstSavedRecord.getSort()).isEqualTo("existing-sort_1");
- assertThat(firstSavedRecord.getRootPartitionKey1()).isEqualTo("existing-rootPk1_1");
- assertThat(firstSavedRecord.getRootPartitionKey2()).isEqualTo("existing-rootPk2_1");
- assertThat(firstSavedRecord.getRootSortKey1()).isEqualTo("existing-rootSk1_1");
- assertThat(firstSavedRecord.getRootSortKey2()).isEqualTo("existing-rootSk2_1");
- assertThat(firstSavedRecord.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_1");
- assertThat(firstSavedRecord.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_1");
- assertThat(firstSavedRecord.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_1");
- assertThat(firstSavedRecord.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_1");
-
-
- BeanWithMixedCompositeGsi secondSavedRecord =
- mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_2").sortValue("existing-sort_2")));
-
- assertThat(secondSavedRecord.getId()).isEqualTo("existing-id_2");
- assertThat(secondSavedRecord.getSort()).isEqualTo("existing-sort_2");
- assertThat(secondSavedRecord.getRootPartitionKey1()).isEqualTo("existing-rootPk1_2");
- assertThat(secondSavedRecord.getRootPartitionKey2()).isEqualTo("existing-rootPk2_2");
- assertThat(secondSavedRecord.getRootSortKey1()).isEqualTo("existing-rootSk1_2");
- assertThat(secondSavedRecord.getRootSortKey2()).isEqualTo("existing-rootSk2_2");
-
- assertThat(secondSavedRecord.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_2");
- assertThat(secondSavedRecord.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_2");
- assertThat(secondSavedRecord.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_2");
- assertThat(secondSavedRecord.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_2");
-
- } finally {
- try {
- mappedTable.deleteTable();
- } catch (Exception ignored) {
- }
- }
- }
-
- @Test
- public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() {
- String tableName = "mixed-gsi-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
-
- try {
- mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
-
- BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
- enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
- .addPutItem(mappedTable, record)
- .build());
-
- BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
-
- isValidUuid(result.getId());
- isValidUuid(result.getSort());
- isValidUuid(result.getRootPartitionKey1());
- isValidUuid(result.getRootPartitionKey2());
- isValidUuid(result.getRootSortKey1());
- isValidUuid(result.getRootSortKey2());
- isValidUuid(result.getFlattenedKeys().flattenedPartitionKey1);
- isValidUuid(result.getFlattenedKeys().flattenedPartitionKey2);
- isValidUuid(result.getFlattenedKeys().flattenedSortKey1);
- isValidUuid(result.getFlattenedKeys().flattenedSortKey2);
-
- } finally {
- try {
- mappedTable.deleteTable();
- } catch (Exception ignored) {
- }
- }
- }
-
- @Test
- public void transactWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() {
- String tableName = "mixed-gsi-autogenerated-key-table";
- DynamoDbTable mappedTable =
- enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class));
-
- try {
- mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
- BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(1);
- enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
- .addPutItem(mappedTable, record)
- .build());
-
- BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
-
- assertThat(result.getId()).isEqualTo("existing-id_1");
- assertThat(result.getSort()).isEqualTo("existing-sort_1");
- assertThat(result.getRootPartitionKey1()).isEqualTo("existing-rootPk1_1");
- assertThat(result.getRootPartitionKey2()).isEqualTo("existing-rootPk2_1");
- assertThat(result.getRootSortKey1()).isEqualTo("existing-rootSk1_1");
- assertThat(result.getRootSortKey2()).isEqualTo("existing-rootSk2_1");
- assertThat(result.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_1");
- assertThat(result.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_1");
- assertThat(result.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_1");
- assertThat(result.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_1");
-
- } finally {
- try {
- mappedTable.deleteTable();
- } catch (Exception ignored) {
- }
- }
- }
-
-
private static BeanWithAutogeneratedKey buildBeanWithAutogeneratedKeyAndKeysPopulated() {
return buildBeanWithAutogeneratedKeyAndKeysPopulated(null);
}
@@ -675,31 +356,6 @@ private static BeanWithAutogeneratedKey buildBeanWithAutogeneratedKeyAndKeysPopu
return record;
}
- private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated() {
- return buildBeanWithCompositeGsiAndKeysPopulated(null);
- }
-
- private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated(Integer index) {
- String suffix = index == null ? "" : "_" + index;
-
- BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
- record.setId("existing-id" + suffix);
- record.setSort("existing-sort" + suffix);
- record.setRootPartitionKey1("existing-rootPk1" + suffix);
- record.setRootPartitionKey2("existing-rootPk2" + suffix);
- record.setRootSortKey1("existing-rootSk1" + suffix);
- record.setRootSortKey2("existing-rootSk2" + suffix);
-
- BeanWithMixedCompositeGsi.FlattenedKeys flattenedKeys = new BeanWithMixedCompositeGsi.FlattenedKeys();
- flattenedKeys.setFlattenedPartitionKey1("existing-flattenedPk1" + suffix);
- flattenedKeys.setFlattenedPartitionKey2("existing-flattenedPk2" + suffix);
- flattenedKeys.setFlattenedSortKey1("existing-flattenedSk1" + suffix);
- flattenedKeys.setFlattenedSortKey2("existing-flattenedSk2" + suffix);
- record.setFlattenedKeys(flattenedKeys);
-
- return record;
- }
-
@DynamoDbBean
public static class BeanWithoutAutogeneratedKey {
@@ -894,142 +550,6 @@ public void setId(String id) {
this.id = id;
}
}
-
- @DynamoDbBean
- public static class BeanWithMixedCompositeGsi {
- private String id;
- private String sort;
- private String rootPartitionKey1;
- private String rootPartitionKey2;
- private String rootSortKey1;
- private String rootSortKey2;
- private FlattenedKeys flattenedKeys;
-
- @DynamoDbPartitionKey
- @DynamoDbAutoGeneratedKey
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- @DynamoDbSortKey
- @DynamoDbAutoGeneratedKey
- public String getSort() {
- return sort;
- }
-
- public void setSort(String sort) {
- this.sort = sort;
- }
-
- @DynamoDbAutoGeneratedKey
- @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
- @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = FIRST)
- public String getRootPartitionKey1() {
- return rootPartitionKey1;
- }
-
- public void setRootPartitionKey1(String rootPartitionKey1) {
- this.rootPartitionKey1 = rootPartitionKey1;
- }
-
- @DynamoDbAutoGeneratedKey
- @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
- @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = SECOND)
- public String getRootPartitionKey2() {
- return rootPartitionKey2;
- }
-
- public void setRootPartitionKey2(String rootPartitionKey2) {
- this.rootPartitionKey2 = rootPartitionKey2;
- }
-
- @DynamoDbAutoGeneratedKey
- @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
- @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = FIRST)
- public String getRootSortKey1() {
- return rootSortKey1;
- }
-
- public void setRootSortKey1(String rootSortKey1) {
- this.rootSortKey1 = rootSortKey1;
- }
-
- @DynamoDbAutoGeneratedKey
- @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
- @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = SECOND)
- public String getRootSortKey2() {
- return rootSortKey2;
- }
-
- public void setRootSortKey2(String rootSortKey2) {
- this.rootSortKey2 = rootSortKey2;
- }
-
- @DynamoDbFlatten
- public FlattenedKeys getFlattenedKeys() {
- return flattenedKeys;
- }
-
- public void setFlattenedKeys(FlattenedKeys flattenedKeys) {
- this.flattenedKeys = flattenedKeys;
- }
-
- @DynamoDbBean
- public static class FlattenedKeys {
- private String flattenedPartitionKey1;
- private String flattenedPartitionKey2;
- private String flattenedSortKey1;
- private String flattenedSortKey2;
-
- @DynamoDbAutoGeneratedKey
- @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
- @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.THIRD)
- public String getFlattenedPartitionKey1() {
- return flattenedPartitionKey1;
- }
-
- public void setFlattenedPartitionKey1(String flattenedPartitionKey1) {
- this.flattenedPartitionKey1 = flattenedPartitionKey1;
- }
-
- @DynamoDbAutoGeneratedKey
- @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
- @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.FOURTH)
- public String getFlattenedPartitionKey2() {
- return flattenedPartitionKey2;
- }
-
- public void setFlattenedPartitionKey2(String flattenedPartitionKey2) {
- this.flattenedPartitionKey2 = flattenedPartitionKey2;
- }
-
- @DynamoDbAutoGeneratedKey
- @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS)
- @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.THIRD)
- public String getFlattenedSortKey1() {
- return flattenedSortKey1;
- }
-
- public void setFlattenedSortKey1(String flattenedSortKey1) {
- this.flattenedSortKey1 = flattenedSortKey1;
- }
-
- @DynamoDbAutoGeneratedKey
- @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
- @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.FOURTH)
- public String getFlattenedSortKey2() {
- return flattenedSortKey2;
- }
-
- public void setFlattenedSortKey2(String flattenedSortKey2) {
- this.flattenedSortKey2 = flattenedSortKey2;
- }
- }
- }
}
From 8c3d7d131db348dc9cb623343c542bb73de4b9ed Mon Sep 17 00:00:00 2001
From: Ana Satirbasa
Date: Mon, 16 Feb 2026 19:25:40 +0200
Subject: [PATCH 12/14] Added tests with composite gsi
---
.../extensions/AutoGeneratedKeyCompositeGsiTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
index 63477ecaa9f9..541d3b80dad3 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
@@ -98,7 +98,7 @@ public void putItem_whenKeysAlreadyPopulated_preservesExistingUuids() {
}
@Test
- public void updateItem_onBeanWithCompositeKeys_respectsUpdateBehavior() {
+ public void updateItem_respectsUpdateBehavior() {
BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated();
// put item with all keys populated
From 6d66b77e9ff2544c70f726e8b18c54cb73a0af48 Mon Sep 17 00:00:00 2001
From: Ana Satirbasa
Date: Mon, 16 Feb 2026 19:27:17 +0200
Subject: [PATCH 13/14] Added tests with composite gsi
---
.../extensions/AutoGeneratedKeyCompositeGsiTest.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
index 541d3b80dad3..49a33d757e71 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
@@ -151,7 +151,7 @@ public void updateItem_respectsUpdateBehavior() {
}
@Test
- public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() {
+ public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1);
BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2);
@@ -170,7 +170,7 @@ public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() {
}
@Test
- public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() {
+ public void batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids() {
BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1);
BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2);
@@ -190,7 +190,7 @@ public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids()
}
@Test
- public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() {
+ public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi();
enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
.addPutItem(mappedTable, record)
@@ -203,7 +203,7 @@ public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids()
}
@Test
- public void transactWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() {
+ public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() {
BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated();
enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder()
.addPutItem(mappedTable, record)
From b935acf200b4b7b9c38f919fc56bd2d869037252 Mon Sep 17 00:00:00 2001
From: Ana Satirbasa
Date: Wed, 18 Feb 2026 16:31:11 +0200
Subject: [PATCH 14/14] Fixed assertions on generated uuids
---
.../AutoGeneratedKeyCompositeGsiTest.java | 27 +++----
.../AutoGeneratedKeyExtensionTest.java | 74 ++++++++++++++-----
.../AutoGeneratedUuidExtensionTest.java | 33 +++++----
3 files changed, 85 insertions(+), 49 deletions(-)
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
index 49a33d757e71..3b872f994ca2 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java
@@ -128,9 +128,6 @@ public void updateItem_respectsUpdateBehavior() {
BeanWithMixedCompositeGsi afterUpdate =
mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSort())));
-
- assertAllKeysAreValidUuids(afterUpdate);
-
// id and sort preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys
assertThat(afterUpdate.getId()).isEqualTo(originalPk);
assertThat(afterUpdate.getSort()).isEqualTo(originalSk);
@@ -152,8 +149,8 @@ public void updateItem_respectsUpdateBehavior() {
@Test
public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
- BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1);
- BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2);
+ BeanWithMixedCompositeGsi firstRecord = new BeanWithMixedCompositeGsi();
+ BeanWithMixedCompositeGsi secondRecord = new BeanWithMixedCompositeGsi();
enhancedClient.batchWriteItem(req -> req.addWriteBatch(
WriteBatch.builder(BeanWithMixedCompositeGsi.class)
@@ -216,16 +213,16 @@ public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() {
}
private static void assertAllKeysAreValidUuids(BeanWithMixedCompositeGsi record) {
- isValidUuid(record.getId());
- isValidUuid(record.getSort());
- isValidUuid(record.getRootPartitionKey1());
- isValidUuid(record.getRootPartitionKey2());
- isValidUuid(record.getRootSortKey1());
- isValidUuid(record.getRootSortKey2());
- isValidUuid(record.getFlattenedKeys().flattenedPartitionKey1);
- isValidUuid(record.getFlattenedKeys().flattenedPartitionKey2);
- isValidUuid(record.getFlattenedKeys().flattenedSortKey1);
- isValidUuid(record.getFlattenedKeys().flattenedSortKey2);
+ assertThat(isValidUuid(record.getId())).isTrue();
+ assertThat(isValidUuid(record.getSort())).isTrue();
+ assertThat(isValidUuid(record.getRootPartitionKey1())).isTrue();
+ assertThat(isValidUuid(record.getRootPartitionKey2())).isTrue();
+ assertThat(isValidUuid(record.getRootSortKey1())).isTrue();
+ assertThat(isValidUuid(record.getRootSortKey2())).isTrue();
+ assertThat(isValidUuid(record.getFlattenedKeys().flattenedPartitionKey1)).isTrue();
+ assertThat(isValidUuid(record.getFlattenedKeys().flattenedPartitionKey2)).isTrue();
+ assertThat(isValidUuid(record.getFlattenedKeys().flattenedSortKey1)).isTrue();
+ assertThat(isValidUuid(record.getFlattenedKeys().flattenedSortKey2)).isTrue();
}
private static void assertCompositeKeyValuesArePreserved(BeanWithMixedCompositeGsi actual) {
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
index 405f87daad95..bc9d4cee5d29 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java
@@ -79,10 +79,10 @@ public void putItem_whenKeysNotPopulated_generatesNewUuids() {
BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst()
.orElseThrow(() -> new AssertionError("No record found"));
- isValidUuid(result.getId());
- isValidUuid(result.getSortKey());
- isValidUuid(result.getGsiPk());
- isValidUuid(result.getGsiSk());
+ assertThat(isValidUuid(result.getId())).isTrue();
+ assertThat(isValidUuid(result.getSortKey())).isTrue();
+ assertThat(isValidUuid(result.getGsiPk())).isTrue();
+ assertThat(isValidUuid(result.getGsiSk())).isTrue();
}
@Test
@@ -126,7 +126,7 @@ public void updateItem_respectsUpdateBehavior() {
assertThat(afterUpdate.getSortKey()).isEqualTo(originalSk);
// gsiPk has WRITE_ALWAYS -> regenerates UUID on every update
- isValidUuid(afterUpdate.getGsiPk());
+ assertThat(isValidUuid(afterUpdate.getGsiPk())).isTrue();
assertThat(afterUpdate.getGsiPk()).isNotEqualTo(originalGsiPk);
// gsiSk has WRITE_IF_NOT_EXISTS -> preserves original UUID, only writes if null
@@ -147,10 +147,10 @@ public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
List results = mappedTable.scan().items().stream().collect(Collectors.toList());
assertThat(results.size()).isEqualTo(2);
- isValidUuid(results.get(0).getId());
- isValidUuid(results.get(1).getId());
- isValidUuid(results.get(0).getSortKey());
- isValidUuid(results.get(1).getSortKey());
+ assertThat(isValidUuid(results.get(0).getId())).isTrue();
+ assertThat(isValidUuid(results.get(1).getId())).isTrue();
+ assertThat(isValidUuid(results.get(0).getSortKey())).isTrue();
+ assertThat(isValidUuid(results.get(1).getSortKey())).isTrue();
}
@Test
@@ -192,10 +192,10 @@ public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst()
.orElseThrow(() -> new AssertionError("No record found"));
- isValidUuid(result.getId());
- isValidUuid(result.getSortKey());
- isValidUuid(result.getGsiPk());
- isValidUuid(result.getGsiSk());
+ assertThat(isValidUuid(result.getId())).isTrue();
+ assertThat(isValidUuid(result.getSortKey())).isTrue();
+ assertThat(isValidUuid(result.getGsiPk())).isTrue();
+ assertThat(isValidUuid(result.getGsiSk())).isTrue();
}
@Test
@@ -217,7 +217,43 @@ public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() {
}
@Test
- public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() {
+ public void putItem_onVersionedRecord_andKeyNotPopulated_worksWithAutoGeneratedKeyAndGeneratesNewUuid() {
+ String tableName = "versioned-record-autogenerated-key-table";
+ DynamoDbTable mappedTable =
+ enhancedClient.table(tableName, TableSchema.fromClass(BeanWithAutogeneratedKeyAndVersion.class));
+
+ try {
+ mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
+
+ BeanWithAutogeneratedKeyAndVersion record = new BeanWithAutogeneratedKeyAndVersion();
+ record.setData("data-v1");
+ mappedTable.putItem(record); // id not set, should be auto generated
+
+ BeanWithAutogeneratedKeyAndVersion retrieved =
+ mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ String generatedUuid = retrieved.getId();
+ assertThat(isValidUuid(generatedUuid)).isTrue();
+ assertThat(retrieved.getData()).isEqualTo("data-v1");
+ assertThat(retrieved.getVersion()).isEqualTo(1L);
+
+ retrieved.setData("data-v2");
+ BeanWithAutogeneratedKeyAndVersion updated = mappedTable.updateItem(retrieved);
+ assertThat(updated.getId()).isEqualTo(generatedUuid);
+ assertThat(updated.getData()).isEqualTo("data-v2");
+ assertThat(updated.getVersion()).isEqualTo(2L);
+
+ } finally {
+ try {
+ mappedTable.deleteTable();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ @Test
+ public void putItem_onVersionedRecord_andAlreadyPopulatedKey_worksWithAutoGeneratedKeyAndPreservesInitialValue() {
String tableName = "versioned-record-autogenerated-key-table";
DynamoDbTable mappedTable =
enhancedClient.table(tableName, TableSchema.fromClass(BeanWithAutogeneratedKeyAndVersion.class));
@@ -230,15 +266,17 @@ public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() {
record.setData("data-v1");
mappedTable.putItem(record);
- BeanWithAutogeneratedKeyAndVersion retrieved = mappedTable.scan().items().stream().findFirst()
- .orElseThrow(() -> new AssertionError("No record found"));
- isValidUuid(retrieved.getId());
+ BeanWithAutogeneratedKeyAndVersion retrieved =
+ mappedTable.scan().items().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No record found"));
+
+ assertThat(retrieved.getId()).isEqualTo("id");
assertThat(retrieved.getData()).isEqualTo("data-v1");
assertThat(retrieved.getVersion()).isEqualTo(1L);
retrieved.setData("data-v2");
BeanWithAutogeneratedKeyAndVersion updated = mappedTable.updateItem(retrieved);
- isValidUuid(updated.getId());
+ assertThat(updated.getId()).isEqualTo("id");
assertThat(updated.getData()).isEqualTo("data-v2");
assertThat(updated.getVersion()).isEqualTo(2L);
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java
index 974d638ed8d9..458e31368d30 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java
@@ -78,8 +78,8 @@ public void putItem_whenKeysNotAlreadyPopulated_generatesNewUuids() {
RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst()
.orElseThrow(() -> new AssertionError("No record found"));
- isValidUuid(result.getId());
- isValidUuid(result.getSortKey());
+ assertThat(isValidUuid(result.getId())).isTrue();
+ assertThat(isValidUuid(result.getSortKey())).isTrue();
}
@Test
@@ -92,8 +92,8 @@ public void putItem_whenKeysAlreadyPopulated_replacesExistingUuids() {
RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst()
.orElseThrow(() -> new AssertionError("No record found"));
- isValidUuid(result.getId());
- isValidUuid(result.getSortKey());
+ assertThat(isValidUuid(result.getId())).isTrue();
+ assertThat(isValidUuid(result.getSortKey())).isTrue();
assertThat(result.getId()).isNotEqualTo("existing-id");
assertThat(result.getSortKey()).isNotEqualTo("existing-sk");
}
@@ -112,10 +112,10 @@ public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
List results = mappedTable.scan().items().stream().collect(Collectors.toList());
assertThat(results.size()).isEqualTo(2);
- isValidUuid(results.get(0).getId());
- isValidUuid(results.get(1).getId());
- isValidUuid(results.get(0).getSortKey());
- isValidUuid(results.get(1).getSortKey());
+ assertThat(isValidUuid(results.get(0).getId())).isTrue();
+ assertThat(isValidUuid(results.get(1).getId())).isTrue();
+ assertThat(isValidUuid(results.get(0).getSortKey())).isTrue();
+ assertThat(isValidUuid(results.get(1).getSortKey())).isTrue();
}
@Test
@@ -138,10 +138,11 @@ public void batchWrite_whenKeysAlreadyPopulated_generatesNewUuids() {
List results = mappedTable.scan().items().stream().collect(Collectors.toList());
assertThat(results.size()).isEqualTo(2);
- isValidUuid(results.get(0).getId());
- isValidUuid(results.get(1).getId());
- isValidUuid(results.get(0).getSortKey());
- isValidUuid(results.get(1).getSortKey());
+ assertThat(results.size()).isEqualTo(2);
+ assertThat(isValidUuid(results.get(0).getId())).isTrue();
+ assertThat(isValidUuid(results.get(1).getId())).isTrue();
+ assertThat(isValidUuid(results.get(0).getSortKey())).isTrue();
+ assertThat(isValidUuid(results.get(1).getSortKey())).isTrue();
assertThat(results.get(0).getId()).isNotEqualTo("existing-id-1");
assertThat(results.get(1).getId()).isNotEqualTo("existing-id-2");
@@ -160,8 +161,8 @@ public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() {
RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst()
.orElseThrow(() -> new AssertionError("No record found"));
- isValidUuid(result.getId());
- isValidUuid(result.getSortKey());
+ assertThat(isValidUuid(result.getId())).isTrue();
+ assertThat(isValidUuid(result.getSortKey())).isTrue();
}
@Test
@@ -177,8 +178,8 @@ public void transactWrite_whenKeysAlreadyPopulated_generatesNewUuids() {
RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst()
.orElseThrow(() -> new AssertionError("No record found"));
- isValidUuid(result.getId());
- isValidUuid(result.getSortKey());
+ assertThat(isValidUuid(result.getId())).isTrue();
+ assertThat(isValidUuid(result.getSortKey())).isTrue();
assertThat(result.getId()).isNotEqualTo("existing-id");
assertThat(result.getSortKey()).isNotEqualTo("existing-sk");
}