diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json new file mode 100644 index 000000000000..8b53718e3746 --- /dev/null +++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon DynamoDB Enhanced Client", + "contributor": "", + "description": "Added strategy-based UUID generation to DynamoDbAutoGeneratedUuid with ALWAYS (default) and CREATE modes." +} diff --git a/services-custom/dynamodb-enhanced/pom.xml b/services-custom/dynamodb-enhanced/pom.xml index 4128c7929622..91bf77dc07f7 100644 --- a/services-custom/dynamodb-enhanced/pom.xml +++ b/services-custom/dynamodb-enhanced/pom.xml @@ -76,6 +76,19 @@ + + com.github.siom79.japicmp + japicmp-maven-plugin + + + + + software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid#strategy() + + + + + 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 d92db8c60bbd..f57ecf4251bf 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,7 +15,6 @@ package software.amazon.awssdk.enhanced.dynamodb.extensions; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -27,17 +26,17 @@ 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.extensions.annotations.DynamoDbAutoGenerateStrategy; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; 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; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.utils.Validate; /** - * This extension facilitates the automatic generation of a unique UUID (Universally Unique Identifier) for a specified attribute - * every time a new record is written to the database. The generated UUID is obtained using the - * {@link java.util.UUID#randomUUID()} method. + * This extension facilitates the automatic generation of a UUID value for tagged string attributes. + * UUID values are generated using {@link java.util.UUID#randomUUID()}. *

* This extension is not loaded by default when you instantiate a * {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Therefore, you need to specify it in a custom @@ -56,22 +55,30 @@ *} *

*

- * To utilize the auto-generated UUID feature, first, create a field in your model that will store the UUID for the attribute. - * This class field must be of type {@link java.lang.String}, and you need to tag it as the autoGeneratedUuidAttribute. If you are + * To use auto-generated UUID values, create a field in your model that stores the UUID attribute. + * This field must be of type {@link java.lang.String}, and you need to tag it as auto-generated UUID. If you are * using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}, then you should use the - * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid} annotation. If you are using + * {@link DynamoDbAutoGeneratedUuid} annotation. If you are using * the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema}, then you should use the * {@link * software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags#autoGeneratedUuidAttribute()} * static attribute tag. *

*

- * Every time a new record is successfully put into the database, the specified attribute will be automatically populated with a - * unique UUID generated using {@link java.util.UUID#randomUUID()}. If the UUID needs to be created only for `putItem` and should - * not be generated for an `updateItem`, then - * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior#WRITE_IF_NOT_EXISTS} must be along with - * {@link DynamoDbUpdateBehavior} - * + * Generation behavior is controlled by {@link DynamoDbAutoGenerateStrategy}: + *

+ *

+ * {@code CREATE} inspects the write item map after mapping, not the value stored in DynamoDB. + * With {@code updateItem} and {@code ignoreNulls(true)}, a null CREATE field is omitted from the map, so a new UUID + * is generated and silently overwrites any existing stored value. + *

+ *

+ * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior} is a separate concern used while building update + * expressions for {@code updateItem}. It does not change this extension's strategy decision. *

*/ @SdkPublicApi @@ -79,7 +86,10 @@ public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientExtension { private static final String CUSTOM_METADATA_KEY = "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; - private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute(); + private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ALWAYS_ATTRIBUTE = + new AutoGeneratedUuidAttribute(DynamoDbAutoGenerateStrategy.ALWAYS); + private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_CREATE_ATTRIBUTE = + new AutoGeneratedUuidAttribute(DynamoDbAutoGenerateStrategy.CREATE); private AutoGeneratedUuidExtension() { } @@ -101,42 +111,69 @@ public static AutoGeneratedUuidExtension create() { public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) { - Collection customMetadataObject = context.tableMetadata() - .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class) - .orElse(null); + Map customMetadataObject = + context.tableMetadata() + .customMetadataObject(CUSTOM_METADATA_KEY, Map.class) + .orElse(null); if (customMetadataObject == null) { return WriteModification.builder().build(); } Map itemToTransform = new HashMap<>(context.items()); - customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key)); + customMetadataObject.forEach((key, strategy) -> insertUuidInItemToTransform(itemToTransform, key, strategy)); return WriteModification.builder() .transformedItem(Collections.unmodifiableMap(itemToTransform)) .build(); } private void insertUuidInItemToTransform(Map itemToTransform, - String key) { + String key, + DynamoDbAutoGenerateStrategy strategy) { + if (strategy == DynamoDbAutoGenerateStrategy.CREATE && !isMissingValue(itemToTransform.get(key))) { + return; + } itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build()); } + private boolean isMissingValue(AttributeValue currentValue) { + return currentValue == null || Boolean.TRUE.equals(currentValue.nul()); + } + public static final class AttributeTags { private AttributeTags() { } /** - * Tags which indicate that the given attribute is supported wih Auto Generated UUID Record Extension. + * Tags which indicate that the given attribute is supported with Auto Generated UUID Record Extension. * * @return Tag name for AutoGenerated UUID Records */ public static StaticAttributeTag autoGeneratedUuidAttribute() { - return AUTO_GENERATED_UUID_ATTRIBUTE; + return AUTO_GENERATED_UUID_ALWAYS_ATTRIBUTE; + } + + /** + * Tags which indicate that the given attribute is supported with Auto Generated UUID Record Extension and + * applies the provided strategy. + * + * @return Tag name for AutoGenerated UUID Records + */ + public static StaticAttributeTag autoGeneratedUuidAttribute(DynamoDbAutoGenerateStrategy strategy) { + Validate.notNull(strategy, "strategy is null"); + return strategy == DynamoDbAutoGenerateStrategy.CREATE + ? AUTO_GENERATED_UUID_CREATE_ATTRIBUTE + : AUTO_GENERATED_UUID_ALWAYS_ATTRIBUTE; } } private static class AutoGeneratedUuidAttribute implements StaticAttributeTag { + private final DynamoDbAutoGenerateStrategy strategy; + + private AutoGeneratedUuidAttribute(DynamoDbAutoGenerateStrategy strategy) { + this.strategy = strategy; + } @Override public void validateType(String attributeName, EnhancedType type, @@ -156,7 +193,8 @@ public void validateType(String attributeName, EnhancedType type, @Override public Consumer modifyMetadata(String attributeName, AttributeValueType attributeValueType) { - return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, Collections.singleton(attributeName)) + return metadata -> metadata.addCustomMetadataObject( + CUSTOM_METADATA_KEY, Collections.singletonMap(attributeName, strategy)) .markAttributeAsKey(attributeName, attributeValueType); } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGenerateStrategy.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGenerateStrategy.java new file mode 100644 index 000000000000..fbed21c7d4c5 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGenerateStrategy.java @@ -0,0 +1,34 @@ +/* + * 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.annotations; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Strategy used to decide when a new value is generated for an annotated attribute. + */ +@SdkPublicApi +public enum DynamoDbAutoGenerateStrategy { + /** + * Generate a new value on every write operation. + */ + ALWAYS, + + /** + * Generate a value only when the current value is missing. + */ + CREATE +} 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..28eec8e676af 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,18 +19,32 @@ 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. + * Denotes this attribute as recording the auto generated UUID string for the record. + *

+ * The {@link #strategy()} controls whether UUID is generated on every write or only when missing. + * The default is {@link DynamoDbAutoGenerateStrategy#ALWAYS} for backward compatibility with existing + * {@code @DynamoDbAutoGeneratedUuid} usage. + * Use {@link DynamoDbAutoGenerateStrategy#CREATE} when you want to generate only if the value is missing + * (absent from the write item map or DynamoDB {@code NULL}). + *

+ * {@code CREATE} inspects the write item map after mapping, not the value stored in DynamoDB. + * With {@code updateItem} and {@code ignoreNulls(true)}, a null CREATE field is omitted from the map, so a new UUID + * is generated and silently overwrites any existing stored value. */ @SdkPublicApi @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @BeanTableSchemaAttributeTag(AutoGeneratedUuidTag.class) public @interface DynamoDbAutoGeneratedUuid { -} \ No newline at end of file + /** + * Defines when a new UUID should be generated. + *

+ * Defaults to {@link DynamoDbAutoGenerateStrategy#ALWAYS} to preserve backward compatibility. + */ + DynamoDbAutoGenerateStrategy strategy() default DynamoDbAutoGenerateStrategy.ALWAYS; +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedUuidTag.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedUuidTag.java index 17872e71954b..499a1db737b8 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedUuidTag.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedUuidTag.java @@ -27,7 +27,7 @@ private AutoGeneratedUuidTag() { } public static StaticAttributeTag attributeTagFor(DynamoDbAutoGeneratedUuid annotation) { - return AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute(); + return AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute(annotation.strategy()); } -} \ No newline at end of file +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java new file mode 100644 index 000000000000..6b4314bc5668 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java @@ -0,0 +1,33 @@ +/* + * 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; + +import java.util.UUID; + +public final class UuidTestUtils { + + private UuidTestUtils() { + } + + public static boolean isValidUuid(String uuid) { + try { + UUID.fromString(uuid); + return true; + } catch (Exception e) { + return false; + } + } +} \ No newline at end of file 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 0a48d5f0ba55..d4568cc073d1 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 @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -26,6 +27,7 @@ import org.junit.jupiter.api.Test; import software.amazon.awssdk.enhanced.dynamodb.OperationContext; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGenerateStrategy; import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.OperationName; @@ -65,97 +67,127 @@ public class AutoGeneratedUuidExtensionTest { .setter(ItemWithUuid::setSimpleString)) .build(); - @Test - public void beforeWrite_schemaWithoutUuidAttribute_returnsEmptyWriteModification() { - StaticTableSchema schemaWithoutUuidMetadata = - StaticTableSchema.builder(ItemWithUuid.class) - .newItemSupplier(ItemWithUuid::new) - .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithUuid::getId) - .setter(ItemWithUuid::setId) - .addTag(primaryPartitionKey())) - .build(); + private static final StaticTableSchema ITEM_WITH_CREATE_UUID_MAPPER = + StaticTableSchema.builder(ItemWithUuid.class) + .newItemSupplier(ItemWithUuid::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithUuid::getId) + .setter(ItemWithUuid::setId) + .addTag(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("uuidAttribute") + .getter(ItemWithUuid::getUuidAttribute) + .setter(ItemWithUuid::setUuidAttribute) + .addTag(AutoGeneratedUuidExtension.AttributeTags + .autoGeneratedUuidAttribute( + DynamoDbAutoGenerateStrategy.CREATE))) + .build(); - ItemWithUuid item = new ItemWithUuid(); - item.setId(RECORD_ID); - Map items = schemaWithoutUuidMetadata.itemToMap(item, true); + private static final StaticTableSchema ITEM_WITHOUT_UUID_MAPPER = + StaticTableSchema.builder(ItemWithUuid.class) + .newItemSupplier(ItemWithUuid::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithUuid::getId) + .setter(ItemWithUuid::setId) + .addTag(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("simpleString") + .getter(ItemWithUuid::getSimpleString) + .setter(ItemWithUuid::setSimpleString)) + .build(); - WriteModification result = uuidExtension.beforeWrite( - DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(schemaWithoutUuidMetadata.tableMetadata()) - .operationName(OperationName.PUT_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build()); + @Test + public void beforeWrite_whenStrategyCreateAndAttributeAbsent_generatesUuid() { + Map rawItems = new HashMap<>(); + rawItems.put("id", AttributeValue.fromS(RECORD_ID)); + + WriteModification result = + uuidExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(rawItems) + .tableMetadata(ITEM_WITH_CREATE_UUID_MAPPER.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); - assertThat(result).usingRecursiveComparison().isEqualTo(WriteModification.builder().build()); + assertThat(isValidUuid(result.transformedItem().get("uuidAttribute").s())).isTrue(); } @Test - public void beforeWrite_updateItemOperation_hasUuidInItem_doesNotCreateUpdateExpressionAndFilters() { - ItemWithUuid SimpleItem = new ItemWithUuid(); - SimpleItem.setId(RECORD_ID); - String uuidAttribute = String.valueOf(UUID.randomUUID()); - SimpleItem.setUuidAttribute(uuidAttribute); + public void beforeWrite_updateItem_whenStrategyCreateAndValueExists_preservesUuid() { + assertUuidBehavior(DynamoDbAutoGenerateStrategy.CREATE, OperationName.UPDATE_ITEM, true, true); + } - Map items = ITEM_WITH_UUID_MAPPER.itemToMap(SimpleItem, true); - assertThat(items).hasSize(2); + @Test + public void beforeWrite_updateItem_whenStrategyCreateAndValueEmptyString_preservesEmptyString() { + Map rawItems = new HashMap<>(); + rawItems.put("id", AttributeValue.fromS(RECORD_ID)); + rawItems.put("uuidAttribute", AttributeValue.fromS("")); WriteModification result = - uuidExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) - .operationName(OperationName.UPDATE_ITEM) - .operationContext(PRIMARY_CONTEXT).build()); - - Map transformedItem = result.transformedItem(); - assertThat(transformedItem).isNotNull().hasSize(2); - assertThat(transformedItem).containsEntry("id", AttributeValue.fromS(RECORD_ID)); - isValidUuid(transformedItem.get("uuidAttribute").s()); - assertThat(result.updateExpression()).isNull(); - + uuidExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(rawItems) + .tableMetadata(ITEM_WITH_CREATE_UUID_MAPPER.tableMetadata()) + .operationName(OperationName.UPDATE_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + assertThat(result.transformedItem().get("uuidAttribute").s()).isEmpty(); } @Test - public void beforeWrite_updateItemOperation_hasNoUuidInItem_doesNotCreatesUpdateExpressionAndFilters() { - ItemWithUuid SimpleItem = new ItemWithUuid(); - SimpleItem.setId(RECORD_ID); + public void beforeWrite_updateItem_whenStrategyCreateAndValueIsDynamoDbNul_generatesUuid() { + Map rawItems = new HashMap<>(); + rawItems.put("id", AttributeValue.fromS(RECORD_ID)); + rawItems.put("uuidAttribute", AttributeValue.fromNul(true)); - Map items = ITEM_WITH_UUID_MAPPER.itemToMap(SimpleItem, true); - assertThat(items).hasSize(1); + WriteModification result = + uuidExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(rawItems) + .tableMetadata(ITEM_WITH_CREATE_UUID_MAPPER.tableMetadata()) + .operationName(OperationName.UPDATE_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + String actualUuid = result.transformedItem().get("uuidAttribute").s(); + assertThat(isValidUuid(actualUuid)).isTrue(); + } + + @Test + public void beforeWrite_updateItem_whenStrategyAlwaysAndValueEmptyString_regeneratesUuid() { + Map rawItems = new HashMap<>(); + rawItems.put("id", AttributeValue.fromS(RECORD_ID)); + rawItems.put("uuidAttribute", AttributeValue.fromS("")); WriteModification result = - uuidExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) - .operationName(OperationName.UPDATE_ITEM) - .operationContext(PRIMARY_CONTEXT).build()); - - Map transformedItem = result.transformedItem(); - assertThat(transformedItem).isNotNull().hasSize(2); - assertThat(transformedItem).containsEntry("id", AttributeValue.fromS(RECORD_ID)); - isValidUuid(transformedItem.get("uuidAttribute").s()); - assertThat(result.updateExpression()).isNull(); + uuidExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(rawItems) + .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) + .operationName(OperationName.UPDATE_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + String actualUuid = result.transformedItem().get("uuidAttribute").s(); + assertThat(isValidUuid(actualUuid)).isTrue(); } @Test - public void beforeWrite_updateItemOperation_UuidNotPresent_newUuidCreated() { + public void beforeWrite_whenNoAutoGeneratedUuidConfigured_returnsEmptyWriteModification() { ItemWithUuid item = new ItemWithUuid(); item.setId(RECORD_ID); + item.setSimpleString("value"); - Map items = ITEM_WITH_UUID_MAPPER.itemToMap(item, true); - assertThat(items).hasSize(1); + WriteModification result = uuidExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(ITEM_WITHOUT_UUID_MAPPER.itemToMap(item, true)) + .tableMetadata(ITEM_WITHOUT_UUID_MAPPER.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); - WriteModification result = - uuidExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_UUID_MAPPER.tableMetadata()) - .operationName(OperationName.UPDATE_ITEM) - .operationContext(PRIMARY_CONTEXT).build()); - assertThat(result.transformedItem()).isNotNull(); + assertThat(result.transformedItem()).isNull(); assertThat(result.updateExpression()).isNull(); - assertThat(result.transformedItem()).hasSize(2); - assertThat(isValidUuid(result.transformedItem().get("uuidAttribute").s())).isTrue(); } @Test @@ -182,6 +214,46 @@ void IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() { + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported."); } + private void assertUuidBehavior(DynamoDbAutoGenerateStrategy strategy, + OperationName operationName, + boolean hasExistingValue, + boolean shouldPreserveExisting) { + ItemWithUuid item = new ItemWithUuid(); + item.setId(RECORD_ID); + String existingUuid = null; + if (hasExistingValue) { + existingUuid = String.valueOf(UUID.randomUUID()); + item.setUuidAttribute(existingUuid); + } + + StaticTableSchema schema = strategy == DynamoDbAutoGenerateStrategy.CREATE + ? ITEM_WITH_CREATE_UUID_MAPPER + : ITEM_WITH_UUID_MAPPER; + Map items = schema.itemToMap(item, true); + + WriteModification result = + uuidExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(schema.tableMetadata()) + .operationName(operationName) + .operationContext(PRIMARY_CONTEXT).build()); + + assertThat(result.updateExpression()).isNull(); + assertThat(result.transformedItem()).containsEntry("id", AttributeValue.fromS(RECORD_ID)); + assertThat(result.transformedItem()).containsKey("uuidAttribute"); + String actualUuid = result.transformedItem().get("uuidAttribute").s(); + + if (shouldPreserveExisting) { + assertThat(actualUuid).isEqualTo(existingUuid); + } else { + assertThat(isValidUuid(actualUuid)).isTrue(); + if (hasExistingValue) { + assertThat(actualUuid).isNotEqualTo(existingUuid); + } + } + } + public static boolean isValidUuid(String uuid) { return UUID_PATTERN.matcher(uuid).matches(); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAutoGeneratedUuidRecordTest.java index 0a134b54abf7..727395adc729 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAutoGeneratedUuidRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AsyncAutoGeneratedUuidRecordTest.java @@ -11,11 +11,12 @@ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. - */ - + */ + package software.amazon.awssdk.enhanced.dynamodb.functionaltests; import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid; import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; @@ -24,10 +25,14 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGenerateStrategy; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; @@ -36,14 +41,74 @@ private static class Record { private String id; private String writeAlwaysUuid; private String writeIfNotExistsUuid; + private FlattenedRecord flattenedRecord; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getWriteAlwaysUuid() { return writeAlwaysUuid; } public void setWriteAlwaysUuid(String writeAlwaysUuid) { this.writeAlwaysUuid = writeAlwaysUuid; } public String getWriteIfNotExistsUuid() { return writeIfNotExistsUuid; } public void setWriteIfNotExistsUuid(String writeIfNotExistsUuid) { this.writeIfNotExistsUuid = writeIfNotExistsUuid; } + + public FlattenedRecord getFlattenedRecord() { + return flattenedRecord; + } + + public void setFlattenedRecord(FlattenedRecord flattenedRecord) { + this.flattenedRecord = flattenedRecord; + } + } + + private static class FlattenedRecord { + private String generated; + + public String getGenerated() { + return generated; + } + + public void setGenerated(String generated) { + this.generated = generated; + } + } + + private static class CreateNonKeyRecord { + private String id; + private String payload; + private String createdUuid; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public String getCreatedUuid() { + return createdUuid; + } + + public void setCreatedUuid(String createdUuid) { + this.createdUuid = createdUuid; + } } + private static final TableSchema FLATTENED_SCHEMA = + StaticTableSchema.builder(FlattenedRecord.class) + .newItemSupplier(FlattenedRecord::new) + .addAttribute(String.class, a -> a.name("generated") + .getter(FlattenedRecord::getGenerated) + .setter(FlattenedRecord::setGenerated) + .tags(autoGeneratedUuidAttribute())) + .build(); + private static final TableSchema TABLE_SCHEMA = StaticTableSchema.builder(Record.class) .newItemSupplier(Record::new) @@ -58,6 +123,23 @@ private static class Record { .setter(Record::setWriteIfNotExistsUuid) .tags(autoGeneratedUuidAttribute(), updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .flatten(FLATTENED_SCHEMA, Record::getFlattenedRecord, Record::setFlattenedRecord) + .build(); + + private static final TableSchema CREATE_NON_KEY_SCHEMA = + StaticTableSchema.builder(CreateNonKeyRecord.class) + .newItemSupplier(CreateNonKeyRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(CreateNonKeyRecord::getId) + .setter(CreateNonKeyRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("payload") + .getter(CreateNonKeyRecord::getPayload) + .setter(CreateNonKeyRecord::setPayload)) + .addAttribute(String.class, a -> a.name("createdUuid") + .getter(CreateNonKeyRecord::getCreatedUuid) + .setter(CreateNonKeyRecord::setCreatedUuid) + .tags(autoGeneratedUuidAttribute(DynamoDbAutoGenerateStrategy.CREATE))) .build(); private final DynamoDbEnhancedAsyncClient enhancedAsyncClient = @@ -69,14 +151,19 @@ private static class Record { private final DynamoDbAsyncTable mappedTable = enhancedAsyncClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + private final DynamoDbAsyncTable createNonKeyTable = + enhancedAsyncClient.table(getConcreteTableName("non-key-create"), CREATE_NON_KEY_SCHEMA); + @Before public void createTable() { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + createNonKeyTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); } @After public void deleteTable() { getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("table-name"))).join(); + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("non-key-create"))).join(); } @Test @@ -98,4 +185,109 @@ public void putOverwrite_withExplicitUuidValues_shouldRegenerateWriteAlwaysAndNo assertThat(persisted.getWriteIfNotExistsUuid()).isNotNull(); UUID.fromString(persisted.getWriteIfNotExistsUuid()); } + + @Test + public void putItem_whenNonKeyCreateAttributeAbsent_generatesUuid() { + CreateNonKeyRecord record = new CreateNonKeyRecord(); + record.setId("id-1"); + record.setPayload("v1"); + createNonKeyTable.putItem(record).join(); + + CreateNonKeyRecord inserted = createNonKeyTable.getItem(r -> r.key(k -> k.partitionValue("id-1"))).join(); + assertThat(isValidUuid(inserted.getCreatedUuid())).isTrue(); + } + + @Test + public void updateItem_whenNonKeyCreateAttributePresent_preservesUuid() { + CreateNonKeyRecord record = new CreateNonKeyRecord(); + record.setId("id-2"); + record.setPayload("v1"); + createNonKeyTable.putItem(record).join(); + + CreateNonKeyRecord inserted = createNonKeyTable.getItem(r -> r.key(k -> k.partitionValue("id-2"))).join(); + String created = inserted.getCreatedUuid(); + inserted.setPayload("v2"); + CreateNonKeyRecord updated = createNonKeyTable.updateItem(inserted).join(); + + assertThat(updated.getCreatedUuid()).isEqualTo(created); + assertThat(updated.getPayload()).isEqualTo("v2"); + } + + @Test + public void updateItem_whenNonKeyCreateAttributeNull_generatesUuid() { + CreateNonKeyRecord record = new CreateNonKeyRecord(); + record.setId("id-3"); + record.setPayload("v1"); + createNonKeyTable.putItem(record).join(); + String created = createNonKeyTable.getItem(r -> r.key(k -> k.partitionValue("id-3"))).join().getCreatedUuid(); + + CreateNonKeyRecord update = new CreateNonKeyRecord(); + update.setId("id-3"); + update.setPayload("v2"); + CreateNonKeyRecord updated = createNonKeyTable.updateItem(update).join(); + + assertThat(isValidUuid(updated.getCreatedUuid())).isTrue(); + assertThat(updated.getCreatedUuid()).isNotEqualTo(created); + assertThat(updated.getPayload()).isEqualTo("v2"); + } + + @Test + public void updateItem_ignoreNullsTrue_whenNonKeyCreateAttributeNull_overwritesStoredUuid() { + CreateNonKeyRecord record = new CreateNonKeyRecord(); + record.setId("id-4"); + record.setPayload("v1"); + createNonKeyTable.putItem(record).join(); + String created = createNonKeyTable.getItem(r -> r.key(k -> k.partitionValue("id-4"))).join().getCreatedUuid(); + + CreateNonKeyRecord update = new CreateNonKeyRecord(); + update.setId("id-4"); + update.setPayload("v2"); + CreateNonKeyRecord updated = createNonKeyTable.updateItem(r -> r.item(update).ignoreNulls(true)).join(); + + assertThat(isValidUuid(updated.getCreatedUuid())).isTrue(); + assertThat(updated.getCreatedUuid()).isNotEqualTo(created); + assertThat(createNonKeyTable.getItem(r -> r.key(k -> k.partitionValue("id-4"))).join().getCreatedUuid()) + .isEqualTo(updated.getCreatedUuid()); + } + + @Test + public void sharedClient_afterDocumentWrite_nonKeyCreateStillGeneratesUuid() { + DynamoDbAsyncTable documentTable = + enhancedAsyncClient.table(getConcreteTableName("document-no-metadata"), + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .build()); + documentTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())).join(); + try { + documentTable.putItem(EnhancedDocument.builder().putString("id", "doc-id").putString("payload", "v1").build()).join(); + + CreateNonKeyRecord record = new CreateNonKeyRecord(); + record.setId("id-shared"); + record.setPayload("v1"); + createNonKeyTable.putItem(record).join(); + + CreateNonKeyRecord inserted = createNonKeyTable.getItem(r -> r.key(k -> k.partitionValue("id-shared"))).join(); + assertThat(isValidUuid(inserted.getCreatedUuid())).isTrue(); + assertThat(documentTable.getItem(r -> r.key(k -> k.partitionValue("doc-id"))).join().getString("payload")) + .isEqualTo("v1"); + } finally { + getDynamoDbAsyncClient().deleteTable(r -> r.tableName(getConcreteTableName("document-no-metadata"))).join(); + } + } + + @Test + public void putAndUpdate_flattenedAlwaysUuid_generatesAndRegenerates() { + Record record = new Record(); + record.setId("id-flat"); + mappedTable.putItem(record).join(); + Record inserted = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-flat"))).join(); + assertThat(isValidUuid(inserted.getFlattenedRecord().getGenerated())).isTrue(); + String generated = inserted.getFlattenedRecord().getGenerated(); + + Record updated = mappedTable.updateItem(inserted).join(); + assertThat(isValidUuid(updated.getFlattenedRecord().getGenerated())).isTrue(); + assertThat(updated.getFlattenedRecord().getGenerated()).isNotEqualTo(generated); + } } 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 a6c207e1be0e..5b0c7a55595e 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,19 +16,19 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; import static java.util.stream.Collectors.toList; +import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid; 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; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; -import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.IntStream; import org.assertj.core.api.Assertions; import org.junit.After; @@ -46,33 +46,24 @@ import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGenerateStrategy; import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; -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.DynamoDbUpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.enhanced.dynamodb.model.ReadBatch; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; -import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; -import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; -import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; @RunWith(Parameterized.class) public class AutoGeneratedUuidRecordTest extends LocalDynamoDbSyncTestBase{ - private static final String UUID_REGEX = - "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"; - - private static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX); - public static void assertValidUuid(String uuid) { - Assertions.assertThat(UUID_PATTERN.matcher(uuid).matches()).isTrue(); + Assertions.assertThat(isValidUuid(uuid)).isTrue(); } private static final String TABLE_NAME = "table-name"; @@ -81,11 +72,11 @@ public static void assertValidUuid(String uuid) { public AutoGeneratedUuidRecordTest(String testName, TableSchema recordTableSchema) { - this.mappedTable = DynamoDbEnhancedClient.builder() - .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedUuidExtension.create()) - .build().table(getConcreteTableName("table-name"), - recordTableSchema); + this.enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedUuidExtension.create()) + .build(); + this.mappedTable = enhancedClient.table(getConcreteTableName("table-name"), recordTableSchema); this.testCaseName = testName; } @@ -115,8 +106,7 @@ public AutoGeneratedUuidRecordTest(String testName, TableSchema recordTa .addAttribute(String.class, a -> a.name("createdUuid") .getter(Record::getCreatedUuid) .setter(Record::createdUuid) - .tags(autoGeneratedUuidAttribute(), - updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .tags(autoGeneratedUuidAttribute(DynamoDbAutoGenerateStrategy.CREATE))) .flatten(FLATTENED_TABLE_SCHEMA, Record::getFlattenedRecord, Record::flattenedRecord) .build(); @@ -126,6 +116,7 @@ public AutoGeneratedUuidRecordTest(String testName, TableSchema recordTa .mapToObj($ -> createUniqueFakeItem()) .map(fakeItem -> TABLE_SCHEMA.itemToMap(fakeItem, true)) .collect(toList()); + private final DynamoDbEnhancedClient enhancedClient; private DynamoDbTable mappedTable; private final String concreteTableName; @@ -201,8 +192,9 @@ public void putItemFollowedByUpdates() { Assertions.assertThat(result.getCreatedUuid()).isNotEqualTo(result.lastUpdatedUuid); Assertions.assertThat(result.getLastUpdatedUuid()).isNotEqualTo(result.flattenedRecord.getGenerated()); - // UPDATE - mappedTable.updateItem(r -> r.item(new Record().id("id").attribute("UpdatedItem"))); + Record fullItemForUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + fullItemForUpdate.attribute("UpdatedItem"); + mappedTable.updateItem(r -> r.item(fullItemForUpdate)); Record afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); assertRecordHasValidUuid(afterUpdate); @@ -211,10 +203,10 @@ public void putItemFollowedByUpdates() { Assertions.assertThat(afterUpdate.getCreatedUuid()).isNotEqualTo(afterUpdate.lastUpdatedUuid); Assertions.assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(afterUpdate.flattenedRecord.getGenerated()); - // UpdateBehavior.WRITE_IF_NOT_EXISTS , the old UUID is not changed + // CREATE strategy preserves existing UUID because the field is present in the update payload. Assertions.assertThat(afterUpdate.getCreatedUuid()).isEqualTo(createdUuidAfterPut); - // UpdateBehavior.WRITE_ALWAYS + // ALWAYS strategy regenerates on each write. Assertions.assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdatedUuiAfterPut); Assertions.assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattenedRecordAfterPut); Assertions.assertThat(afterUpdate.getAttribute()).isEqualTo("UpdatedItem"); @@ -243,10 +235,10 @@ public void putExistingRecordWithConditionExpressions() { Record afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("newId"))); - // UpdateBehavior.WRITE_IF_NOT_EXISTS , this gets changed because this is a put + // CREATE strategy generates on put when the value is missing from the request item. Assertions.assertThat(afterUpdate.getCreatedUuid()).isNotEqualTo(createdUuidAfterPut); - // UpdateBehavior.WRITE_ALWAYS + // ALWAYS strategy regenerates on each write. Assertions.assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdatedUuiAfterPut); Assertions.assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattenedRecordAfterPut); Assertions.assertThat(afterUpdate.getAttribute()).isEqualTo("conditionalUpdate"); @@ -268,12 +260,14 @@ public void updateExistingRecordWithConditionExpressions() { .putExpressionValue(":v1", stringValue("wrong2")) .build(); - mappedTable.updateItem(r -> r.item(new Record().id("id").attribute("conditionalUpdate")) + Record fullItemForConditionalUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + fullItemForConditionalUpdate.attribute("conditionalUpdate"); + mappedTable.updateItem(r -> r.item(fullItemForConditionalUpdate) .conditionExpression(conditionExpression)); Record afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - // UpdateBehavior.WRITE_IF_NOT_EXISTS , this gets changed because this is a put + // CREATE strategy preserves when the existing value is supplied in the update payload. Assertions.assertThat(afterUpdate.getCreatedUuid()).isEqualTo(createdUuidAfterPut); - // UpdateBehavior.WRITE_ALWAYS + // ALWAYS strategy regenerates on each write. Assertions.assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdatedUuiAfterPut); Assertions.assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattenedRecordAfterPut); Assertions.assertThat(afterUpdate.getAttribute()).isEqualTo("conditionalUpdate"); @@ -315,7 +309,7 @@ public void updateItemConditionTestFailure() { } @Test - public void putOverwrite_withExplicitUuidValues_shouldRegenerateAllUuidsAffectedByBehavior() { + public void putOverwrite_withExplicitUuidValues_preservesCreateAndRegeneratesAlways() { mappedTable.putItem(r -> r.item(new Record().id("overwrite-id").attribute("one"))); Record first = mappedTable.getItem(r -> r.key(k -> k.partitionValue("overwrite-id"))); @@ -327,15 +321,117 @@ public void putOverwrite_withExplicitUuidValues_shouldRegenerateAllUuidsAffected mappedTable.putItem(overwrite); Record second = mappedTable.getItem(r -> r.key(k -> k.partitionValue("overwrite-id"))); - assertRecordHasValidUuid(second); - Assertions.assertThat(second.getCreatedUuid()).isNotEqualTo(overwrite.getCreatedUuid()); + Assertions.assertThat(second.getCreatedUuid()).isEqualTo(overwrite.getCreatedUuid()); Assertions.assertThat(second.getLastUpdatedUuid()).isNotEqualTo(overwrite.getLastUpdatedUuid()); Assertions.assertThat(second.getFlattenedRecord().getGenerated()) .isNotEqualTo(overwrite.getFlattenedRecord().getGenerated()); + assertValidUuid(second.getLastUpdatedUuid()); + assertValidUuid(second.getFlattenedRecord().getGenerated()); Assertions.assertThat(second.getCreatedUuid()).isNotEqualTo(first.getCreatedUuid()); Assertions.assertThat(second.getAttribute()).isEqualTo("two"); } + @Test + public void updateItem_whenNonKeyCreateAttributeNull_generatesUuid() { + mappedTable.putItem(new Record().id("id-3").attribute("v1")); + String created = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-3"))).getCreatedUuid(); + + Record updated = mappedTable.updateItem(new Record().id("id-3").attribute("v2")); + + assertValidUuid(updated.getCreatedUuid()); + Assertions.assertThat(updated.getCreatedUuid()).isNotEqualTo(created); + Assertions.assertThat(updated.getAttribute()).isEqualTo("v2"); + } + + @Test + public void updateItem_ignoreNullsTrue_whenNonKeyCreateAttributeNull_overwritesStoredUuid() { + mappedTable.putItem(new Record().id("id-4").attribute("v1")); + String created = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-4"))).getCreatedUuid(); + + Record updated = mappedTable.updateItem(r -> r.item(new Record().id("id-4").attribute("v2")).ignoreNulls(true)); + + assertValidUuid(updated.getCreatedUuid()); + Assertions.assertThat(updated.getCreatedUuid()).isNotEqualTo(created); + Assertions.assertThat(mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-4"))).getCreatedUuid()) + .isEqualTo(updated.getCreatedUuid()); + } + + @Test + public void query_whenNonKeyCreateGenerated_returnsItem() { + mappedTable.putItem(new Record().id("id-q").attribute("v1")); + + List results = mappedTable.query(r -> r.queryConditional(keyEqualTo(k -> k.partitionValue("id-q")))) + .items() + .stream() + .collect(Collectors.toList()); + + Assertions.assertThat(results).hasSize(1); + assertValidUuid(results.get(0).getCreatedUuid()); + Assertions.assertThat(results.get(0).getAttribute()).isEqualTo("v1"); + } + + @Test + public void deleteItem_whenItemExists_removesRecord() { + mappedTable.putItem(new Record().id("id-d").attribute("v1")); + mappedTable.deleteItem(r -> r.key(k -> k.partitionValue("id-d"))); + + Assertions.assertThat(mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-d")))).isNull(); + } + + @Test + public void putItem_whenCreateAttributeIsEmptyString_preservesEmptyString() { + mappedTable.putItem(new Record().id("id-e").attribute("v1").createdUuid("")); + + Record inserted = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-e"))); + Assertions.assertThat(inserted.getCreatedUuid()).isEmpty(); + } + + @Test + public void updateItem_whenCreateAttributeIsEmptyString_preservesEmptyString() { + mappedTable.putItem(new Record().id("id-eu").attribute("v1").createdUuid("")); + Record loaded = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-eu"))); + loaded.attribute("v2"); + + Record updated = mappedTable.updateItem(loaded); + + Assertions.assertThat(updated.getCreatedUuid()).isEmpty(); + Assertions.assertThat(updated.getAttribute()).isEqualTo("v2"); + } + + @Test + public void flattenedUuid_acrossPutUpdateScanBatchGet_preservesCreateAndRegeneratesAlways() { + mappedTable.putItem(new Record().id("id-chain").attribute("one")); + Record afterPut = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-chain"))); + String created = afterPut.getCreatedUuid(); + String lastUpdated = afterPut.getLastUpdatedUuid(); + String flattened = afterPut.getFlattenedRecord().getGenerated(); + + Record loaded = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-chain"))); + loaded.attribute("two"); + mappedTable.updateItem(loaded); + + Record afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id-chain"))); + Assertions.assertThat(afterUpdate.getCreatedUuid()).isEqualTo(created); + Assertions.assertThat(afterUpdate.getLastUpdatedUuid()).isNotEqualTo(lastUpdated); + Assertions.assertThat(afterUpdate.getFlattenedRecord().getGenerated()).isNotEqualTo(flattened); + + List scanned = mappedTable.scan().items().stream().collect(Collectors.toList()); + Assertions.assertThat(scanned).hasSize(1); + Assertions.assertThat(scanned.get(0).getCreatedUuid()).isEqualTo(created); + + List batchRead = enhancedClient.batchGetItem(r -> r.readBatches( + ReadBatch.builder(Record.class) + .mappedTableResource(mappedTable) + .addGetItem(i -> i.key(k -> k.partitionValue("id-chain"))) + .build())) + .resultsForTable(mappedTable) + .stream() + .collect(Collectors.toList()); + Assertions.assertThat(batchRead).hasSize(1); + Assertions.assertThat(batchRead.get(0).getCreatedUuid()).isEqualTo(created); + Assertions.assertThat(batchRead.get(0).getAttribute()).isEqualTo("two"); + } + public static Record createUniqueFakeItem() { Record record = new Record(); record.setId(UUID.randomUUID().toString()); @@ -389,8 +485,7 @@ public Record lastUpdatedUuid(String lastUpdatedUuid) { return this; } - @DynamoDbAutoGeneratedUuid - @DynamoDbUpdateBehavior(value = UpdateBehavior.WRITE_IF_NOT_EXISTS) + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) public String getCreatedUuid() { return createdUuid; } @@ -498,4 +593,5 @@ public String toString() { '}'; } } + } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidStrategyCompositeGsiTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidStrategyCompositeGsiTest.java new file mode 100644 index 000000000000..40c55cdb2efe --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidStrategyCompositeGsiTest.java @@ -0,0 +1,442 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; +import static 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 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.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGenerateStrategy; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +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.model.EnhancedLocalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; +import software.amazon.awssdk.services.dynamodb.model.Projection; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class AutoGeneratedUuidStrategyCompositeGsiTest extends LocalDynamoDbSyncTestBase { + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(BeanWithMixedCompositeGsi.class); + + private final DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedUuidExtension.create()) + .build(); + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName("mixed-gsi-autogenerated-uuid-strategy-table"), TABLE_SCHEMA); + + private final DynamoDbTable lsiTable = + enhancedClient.table(getConcreteTableName("lsi-autogenerated-uuid-create-table"), + TableSchema.fromClass(BeanWithLsiCreate.class)); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + lsiTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()) + .localSecondaryIndices( + EnhancedLocalSecondaryIndex.create( + "lsi1", + Projection.builder().projectionType(ProjectionType.ALL).build()))); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("mixed-gsi-autogenerated-uuid-strategy-table"))); + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("lsi-autogenerated-uuid-create-table"))); + } + + @Test + public void create_compositeGsi_whenKeysNotPopulated_generatesExpectedUuids() { + BeanWithMixedCompositeGsi result = putAndGetSingleRecord(new BeanWithMixedCompositeGsi()); + assertAllKeysAreValidUuids(result); + } + + @Test + public void create_compositeGsi_whenKeysAlreadyPopulatedAndStrategyCreate_preservesExistingValues() { + BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); + BeanWithMixedCompositeGsi result = putAndGetSingleRecord(record); + + assertCompositeKeyValuesArePreserved(result); + } + + @Test + public void update_compositeGsi_whenMixedStrategiesConfigured_appliesPreserveAndRegenerateBehavior() { + BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); + BeanWithMixedCompositeGsi afterPut = putAndGetSingleRecord(record); + + // Use the full loaded item for update so CREATE strategy fields are present in the write payload. + mappedTable.updateItem(afterPut); + BeanWithMixedCompositeGsi afterUpdate = getByPrimaryKey(afterPut.getId(), afterPut.getSort()); + + assertCreateStrategyFieldsPreserved(afterPut, afterUpdate); + assertAlwaysStrategyFieldsRegenerated(afterPut, afterUpdate); + } + + @Test + public void batchwrite_compositeGsi_whenKeysNotPopulated_generatesExpectedUuidsForAllItems() { + BeanWithMixedCompositeGsi firstRecord = new BeanWithMixedCompositeGsi(); + BeanWithMixedCompositeGsi secondRecord = new BeanWithMixedCompositeGsi(); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(BeanWithMixedCompositeGsi.class) + .mappedTableResource(mappedTable) + .addPutItem(firstRecord) + .addPutItem(secondRecord) + .build())); + + List results = scanItems(); + assertThat(results.size()).isEqualTo(2); + assertAllKeysAreValidUuids(results.get(0)); + assertAllKeysAreValidUuids(results.get(1)); + } + + @Test + public void batchwrite_compositeGsi_whenKeysAlreadyPopulatedAndStrategyCreate_preservesCreateValues() { + 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 = getByPrimaryKey("existing-id_1", "existing-sort_1"); + assertCompositeKeyValuesArePreserved(firstSavedRecord, 1); + + BeanWithMixedCompositeGsi secondSavedRecord = getByPrimaryKey("existing-id_2", "existing-sort_2"); + assertCompositeKeyValuesArePreserved(secondSavedRecord, 2); + } + + @Test + public void transactwrite_compositeGsi_whenKeysNotPopulated_generatesExpectedUuids() { + BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); + enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + BeanWithMixedCompositeGsi result = scanItems().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertAllKeysAreValidUuids(result); + } + + @Test + public void transactwrite_compositeGsi_whenKeysAlreadyPopulatedAndStrategyCreate_preservesCreateValues() { + BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); + enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + BeanWithMixedCompositeGsi result = scanItems().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertCompositeKeyValuesArePreserved(result); + } + + @Test + public void putItem_whenLsiSortKeyAbsent_generatesUuid() { + BeanWithLsiCreate record = new BeanWithLsiCreate(); + record.setId("pk-1"); + record.setSort("sk-1"); + lsiTable.putItem(record); + + BeanWithLsiCreate stored = lsiTable.getItem(r -> r.key(k -> k.partitionValue("pk-1").sortValue("sk-1"))); + assertThat(isValidUuid(stored.getLsiSort())).isTrue(); + } + + @Test + public void updateItem_whenLsiSortKeyPresent_preservesUuid() { + BeanWithLsiCreate record = new BeanWithLsiCreate(); + record.setId("pk-2"); + record.setSort("sk-2"); + lsiTable.putItem(record); + + BeanWithLsiCreate loaded = lsiTable.getItem(r -> r.key(k -> k.partitionValue("pk-2").sortValue("sk-2"))); + String generated = loaded.getLsiSort(); + BeanWithLsiCreate updated = lsiTable.updateItem(loaded); + + assertThat(updated.getLsiSort()).isEqualTo(generated); + } + + private static void assertAllKeysAreValidUuids(BeanWithMixedCompositeGsi record) { + 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) { + 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); + + // ALWAYS fields are expected to regenerate each write; assert create-strategy fields only. + assertThat(actual.getRootPartitionKey2()).isEqualTo("existing-rootPk2" + suffix); + assertThat(actual.getRootSortKey2()).isEqualTo("existing-rootSk2" + suffix); + assertThat(actual.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2" + suffix); + assertThat(actual.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2" + suffix); + } + + private static void assertCreateStrategyFieldsPreserved(BeanWithMixedCompositeGsi before, + BeanWithMixedCompositeGsi after) { + assertThat(after.getId()).isEqualTo(before.getId()); + assertThat(after.getSort()).isEqualTo(before.getSort()); + assertThat(after.getRootPartitionKey2()).isEqualTo(before.getRootPartitionKey2()); + assertThat(after.getRootSortKey2()).isEqualTo(before.getRootSortKey2()); + assertThat(after.getFlattenedKeys().flattenedPartitionKey2).isEqualTo(before.getFlattenedKeys().flattenedPartitionKey2); + assertThat(after.getFlattenedKeys().flattenedSortKey2).isEqualTo(before.getFlattenedKeys().flattenedSortKey2); + } + + private static void assertAlwaysStrategyFieldsRegenerated(BeanWithMixedCompositeGsi before, + BeanWithMixedCompositeGsi after) { + assertThat(after.getRootPartitionKey1()).isNotEqualTo(before.getRootPartitionKey1()); + assertThat(after.getRootSortKey1()).isNotEqualTo(before.getRootSortKey1()); + assertThat(after.getFlattenedKeys().flattenedPartitionKey1).isNotEqualTo(before.getFlattenedKeys().flattenedPartitionKey1); + assertThat(after.getFlattenedKeys().flattenedSortKey1).isNotEqualTo(before.getFlattenedKeys().flattenedSortKey1); + } + + private BeanWithMixedCompositeGsi putAndGetSingleRecord(BeanWithMixedCompositeGsi record) { + mappedTable.putItem(record); + return scanItems().stream().findFirst().orElseThrow(() -> new AssertionError("No record found")); + } + + private BeanWithMixedCompositeGsi getByPrimaryKey(String partitionKey, String sortKey) { + return mappedTable.getItem(r -> r.key(k -> k.partitionValue(partitionKey).sortValue(sortKey))); + } + + private List scanItems() { + return mappedTable.scan().items().stream().collect(Collectors.toList()); + } + + 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 + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.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; + } + + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + @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; + } + + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.ALWAYS) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = FIRST) + public String getRootSortKey1() { + return rootSortKey1; + } + + public void setRootSortKey1(String rootSortKey1) { + this.rootSortKey1 = rootSortKey1; + } + + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + @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; + + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.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; + } + + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.FOURTH) + public String getFlattenedPartitionKey2() { + return flattenedPartitionKey2; + } + + public void setFlattenedPartitionKey2(String flattenedPartitionKey2) { + this.flattenedPartitionKey2 = flattenedPartitionKey2; + } + + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.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; + } + + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.FOURTH) + public String getFlattenedSortKey2() { + return flattenedSortKey2; + } + + public void setFlattenedSortKey2(String flattenedSortKey2) { + this.flattenedSortKey2 = flattenedSortKey2; + } + } + } + + @DynamoDbBean + public static class BeanWithLsiCreate { + private String id; + private String sort; + private String lsiSort; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbSecondarySortKey(indexNames = "lsi1") + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + public String getLsiSort() { + return lsiSort; + } + + public void setLsiSort(String lsiSort) { + this.lsiSort = lsiSort; + } + } +} + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidStrategyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidStrategyRecordTest.java new file mode 100644 index 000000000000..71177c9c1052 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidStrategyRecordTest.java @@ -0,0 +1,842 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.junit.After; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.Document; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGenerateStrategy; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; +import software.amazon.awssdk.enhanced.dynamodb.model.ReadBatch; +import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; + +public class AutoGeneratedUuidStrategyRecordTest extends LocalDynamoDbSyncTestBase { + private final List createdTables = new ArrayList<>(); + + private DynamoDbEnhancedClient enhancedClient() { + return DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedUuidExtension.create(), + VersionedRecordExtension.builder().build()) + .build(); + } + + @After + public void deleteTables() { + createdTables.forEach(tableName -> { + try { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder().tableName(tableName).build()); + } catch (ResourceNotFoundException ignored) { + } + }); + createdTables.clear(); + } + + @Test + public void create_bean_whenUuidMissingAndStrategyCreate_generatesUuid() { + DynamoDbTable table = + createTableResource(newTableName("bean-create"), TableSchema.fromBean(CreateBeanRecord.class)); + + CreateBeanRecord inserted = table.updateItem(new CreateBeanRecord().setPayload("v1")); + + assertUuidGenerated(inserted.getId()); + assertThat(inserted.getPayload()).isEqualTo("v1"); + } + + @Test + public void update_bean_whenPkUsesStrategyCreate_updatesExistingItem() { + DynamoDbTable table = + createTableResource(newTableName("bean-update-create"), TableSchema.fromBean(CreateBeanRecord.class)); + + CreateBeanRecord inserted = table.updateItem(new CreateBeanRecord().setPayload("v1")); + CreateBeanRecord updated = table.updateItem(new CreateBeanRecord().setId(inserted.getId()).setPayload("v2")); + + assertUuidPreserved(inserted.getId(), updated.getId()); + assertThat(updated.getPayload()).isEqualTo("v2"); + } + + @Test + public void update_bean_whenPkUsesStrategyAlways_createsDifferentItem() { + DynamoDbTable table = + createTableResource(newTableName("bean-update-always"), TableSchema.fromBean(AlwaysBeanRecord.class)); + + AlwaysBeanRecord inserted = table.updateItem(new AlwaysBeanRecord().setPayload("v1")); + AlwaysBeanRecord updated = table.updateItem(new AlwaysBeanRecord().setId(inserted.getId()).setPayload("v2")); + + assertUuidRegenerated(inserted.getId(), updated.getId()); + assertThat(table.getItem(r -> r.key(k -> k.partitionValue(inserted.getId()))).getPayload()).isEqualTo("v1"); + assertThat(table.getItem(r -> r.key(k -> k.partitionValue(updated.getId()))).getPayload()).isEqualTo("v2"); + } + + @Test + public void update_bean_whenVersionedPkUsesStrategyCreate_incrementsVersionSuccessfully() { + DynamoDbTable table = + createTableResource(newTableName("bean-version-create"), TableSchema.fromBean(VersionedCreateBeanRecord.class)); + + VersionedCreateBeanRecord inserted = table.updateItem(new VersionedCreateBeanRecord().setPayload("v1")); + VersionedCreateBeanRecord loaded = table.getItem(r -> r.key(k -> k.partitionValue(inserted.getId()))); + loaded.setPayload("v2"); + + VersionedCreateBeanRecord updated = table.updateItem(loaded); + + assertUuidPreserved(inserted.getId(), updated.getId()); + assertVersionIncremented(loaded.getVersion(), updated.getVersion()); + } + + @Test + public void update_bean_whenVersionedPkUsesStrategyAlways_throwsConditionalCheckFailedException() { + DynamoDbTable table = + createTableResource(newTableName("bean-version-always"), TableSchema.fromBean(VersionedAlwaysBeanRecord.class)); + + VersionedAlwaysBeanRecord inserted = table.updateItem(new VersionedAlwaysBeanRecord().setPayload("v1")); + VersionedAlwaysBeanRecord loaded = table.getItem(r -> r.key(k -> k.partitionValue(inserted.getId()))); + loaded.setPayload("v2"); + + assertThatExceptionOfType(ConditionalCheckFailedException.class) + .isThrownBy(() -> table.updateItem(loaded)); + } + + @Test + public void update_static_whenPkUsesStrategyCreate_updatesExistingItem() { + DynamoDbTable table = createTableResource(newTableName("static-create"), STATIC_CREATE_SCHEMA); + + StaticRecord inserted = table.updateItem(new StaticRecord().setPayload("v1")); + StaticRecord updated = table.updateItem(new StaticRecord().setId(inserted.getId()).setPayload("v2")); + + assertUuidPreserved(inserted.getId(), updated.getId()); + assertThat(updated.getPayload()).isEqualTo("v2"); + } + + @Test + public void update_immutable_whenPkUsesStrategyCreate_updatesExistingItem() { + DynamoDbTable table = + createTableResource(newTableName("immutable-create"), TableSchema.fromImmutableClass(ImmutableRecord.class)); + + ImmutableRecord inserted = table.updateItem(ImmutableRecord.builder().payload("v1").build()); + ImmutableRecord updated = table.updateItem(ImmutableRecord.builder().id(inserted.id()).payload("v2").build()); + + assertUuidPreserved(inserted.id(), updated.id()); + assertThat(updated.payload()).isEqualTo("v2"); + } + + @Test + public void update_staticimmutable_whenPkUsesStrategyCreate_updatesExistingItem() { + DynamoDbTable table = + createTableResource(newTableName("static-immutable-create"), STATIC_IMMUTABLE_CREATE_SCHEMA); + + StaticImmutableRecord inserted = table.updateItem(StaticImmutableRecord.builder().payload("v1").build()); + StaticImmutableRecord updated = table.updateItem(StaticImmutableRecord.builder() + .id(inserted.id()) + .payload("v2") + .build()); + + assertUuidPreserved(inserted.id(), updated.id()); + assertThat(updated.payload()).isEqualTo("v2"); + } + + @Test + public void update_convertedby_whenPkUsesStrategyCreate_updatesExistingItem() { + DynamoDbTable table = + createTableResource(newTableName("convertedby-create"), TableSchema.fromBean(ConvertedBeanRecord.class)); + + ConvertedBeanRecord inserted = + table.updateItem(new ConvertedBeanRecord().setPayload(new ConvertedPayload("v1"))); + ConvertedBeanRecord updated = + table.updateItem(new ConvertedBeanRecord().setId(inserted.getId()).setPayload(new ConvertedPayload("v2"))); + + assertUuidPreserved(inserted.getId(), updated.getId()); + assertThat(updated.getPayload().value()).isEqualTo("v2"); + } + + @Test + public void subsequentoperations_bean_whenPkUsesStrategyCreate_batchWriteAndTransactWriteBehaveAsExpected() { + DynamoDbEnhancedClient enhancedClient = enhancedClient(); + DynamoDbTable table = + createTableResource(newTableName("subsequent-ops-create"), TableSchema.fromBean(CreateBeanRecord.class)); + + CreateBeanRecord first = table.updateItem(new CreateBeanRecord().setPayload("first")); + CreateBeanRecord second = new CreateBeanRecord().setPayload("second"); + enhancedClient.batchWriteItem(r -> r.addWriteBatch(WriteBatch.builder(CreateBeanRecord.class) + .mappedTableResource(table) + .addPutItem(second) + .build())); + + CreateBeanRecord batchInserted = table.scan().items().stream() + .filter(item -> "second".equals(item.getPayload())) + .findFirst() + .orElseThrow(IllegalStateException::new); + assertUuidGenerated(batchInserted.getId()); + + CreateBeanRecord transactUpdated = + new CreateBeanRecord().setId(first.getId()).setPayload("first-updated"); + enhancedClient.transactWriteItems(r -> r.addUpdateItem(table, transactUpdated)); + + assertThat(table.getItem(r -> r.key(k -> k.partitionValue(first.getId()))).getPayload()).isEqualTo("first-updated"); + } + + @Test + public void putItem_whenPkCreateAndIdNull_generatesUuid() { + DynamoDbTable table = + createTableResource(newTableName("put-pk-create"), TableSchema.fromBean(CreateBeanRecord.class)); + + table.putItem(new CreateBeanRecord().setPayload("v1")); + CreateBeanRecord inserted = table.scan().items().stream() + .findFirst() + .orElseThrow(IllegalStateException::new); + + assertUuidGenerated(inserted.getId()); + assertThat(inserted.getPayload()).isEqualTo("v1"); + } + + @Test + public void query_whenPkCreate_returnsItemByGeneratedKey() { + DynamoDbTable table = + createTableResource(newTableName("query-pk-create"), TableSchema.fromBean(CreateBeanRecord.class)); + + CreateBeanRecord inserted = table.updateItem(new CreateBeanRecord().setPayload("v1")); + List results = + table.query(r -> r.queryConditional(QueryConditional.keyEqualTo(k -> k.partitionValue(inserted.getId())))) + .items() + .stream() + .collect(Collectors.toList()); + + assertThat(results).hasSize(1); + assertUuidPreserved(inserted.getId(), results.get(0).getId()); + assertThat(results.get(0).getPayload()).isEqualTo("v1"); + } + + @Test + public void batchGetItem_whenTwoCreateItems_returnsGeneratedKeys() { + DynamoDbEnhancedClient enhancedClient = enhancedClient(); + DynamoDbTable table = + enhancedClient.table(newTableName("batch-get-create"), TableSchema.fromBean(CreateBeanRecord.class)); + createTable(table); + + CreateBeanRecord first = table.updateItem(new CreateBeanRecord().setPayload("a")); + CreateBeanRecord second = table.updateItem(new CreateBeanRecord().setPayload("b")); + + List results = + enhancedClient.batchGetItem(r -> r.readBatches( + ReadBatch.builder(CreateBeanRecord.class) + .mappedTableResource(table) + .addGetItem(i -> i.key(k -> k.partitionValue(first.getId()))) + .addGetItem(i -> i.key(k -> k.partitionValue(second.getId()))) + .build())) + .resultsForTable(table) + .stream() + .collect(Collectors.toList()); + + assertThat(results).hasSize(2); + assertThat(results.stream().map(CreateBeanRecord::getId)).containsExactlyInAnyOrder(first.getId(), second.getId()); + } + + @Test + public void transactGetItems_whenCreateItem_returnsGeneratedKey() { + DynamoDbEnhancedClient enhancedClient = enhancedClient(); + DynamoDbTable table = + enhancedClient.table(newTableName("transact-get-create"), TableSchema.fromBean(CreateBeanRecord.class)); + createTable(table); + + CreateBeanRecord inserted = table.updateItem(new CreateBeanRecord().setPayload("v1")); + List results = enhancedClient.transactGetItems( + r -> r.addGetItem(table, Key.builder().partitionValue(inserted.getId()).build())); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getItem(table).getId()).isEqualTo(inserted.getId()); + assertThat(results.get(0).getItem(table).getPayload()).isEqualTo("v1"); + } + + @Test + public void update_document_whenNoAutoGeneratedMetadata_present_doesNotGenerateUuid() { + DynamoDbTable table = + createTableResource(newTableName("document-no-metadata"), + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .build()); + + table.updateItem(EnhancedDocument.builder().putString("id", "doc-id").putString("payload", "v1").build()); + EnhancedDocument updated = table.updateItem(EnhancedDocument.builder() + .putString("id", "doc-id") + .putString("payload", "v2") + .build()); + assertThat(updated.getString("id")).isEqualTo("doc-id"); + assertThat(updated.getString("payload")).isEqualTo("v2"); + } + + @Test + public void annotationAndStaticBuilder_onSharedClient_bothGenerateUuid() { + DynamoDbEnhancedClient enhancedClient = enhancedClient(); + DynamoDbTable beanTable = + enhancedClient.table(newTableName("annotation-create"), TableSchema.fromBean(CreateBeanRecord.class)); + DynamoDbTable staticTable = + enhancedClient.table(newTableName("builder-create"), STATIC_CREATE_SCHEMA); + createTable(beanTable); + createTable(staticTable); + + CreateBeanRecord beanInserted = beanTable.updateItem(new CreateBeanRecord().setPayload("bean")); + StaticRecord staticInserted = staticTable.updateItem(new StaticRecord().setPayload("static")); + + assertUuidGenerated(beanInserted.getId()); + assertUuidGenerated(staticInserted.getId()); + } + + @Test + public void sharedClient_afterDocumentWrite_beanCreateStillGeneratesUuid() { + DynamoDbEnhancedClient enhancedClient = enhancedClient(); + DynamoDbTable documentTable = + enhancedClient.table(newTableName("document-then-bean"), documentSchema()); + DynamoDbTable beanTable = + enhancedClient.table(newTableName("bean-after-document"), TableSchema.fromBean(CreateBeanRecord.class)); + createTable(documentTable); + createTable(beanTable); + + documentTable.putItem(EnhancedDocument.builder().putString("id", "doc-id").putString("payload", "v1").build()); + CreateBeanRecord inserted = beanTable.updateItem(new CreateBeanRecord().setPayload("v1")); + + assertThat(documentTable.getItem(r -> r.key(k -> k.partitionValue("doc-id"))).getString("payload")).isEqualTo("v1"); + assertUuidGenerated(inserted.getId()); + } + + @Test + public void batchGet_mixedBeanStaticAndDocument_returnsItems() { + DynamoDbEnhancedClient enhancedClient = enhancedClient(); + DynamoDbTable beanTable = + enhancedClient.table(newTableName("mixed-get-bean"), TableSchema.fromBean(CreateBeanRecord.class)); + DynamoDbTable staticTable = + enhancedClient.table(newTableName("mixed-get-static"), STATIC_CREATE_SCHEMA); + DynamoDbTable immutableTable = + enhancedClient.table(newTableName("mixed-get-immutable"), TableSchema.fromImmutableClass(ImmutableRecord.class)); + DynamoDbTable documentTable = + enhancedClient.table(newTableName("mixed-get-document"), documentSchema()); + createTable(beanTable); + createTable(staticTable); + createTable(immutableTable); + createTable(documentTable); + + CreateBeanRecord bean = beanTable.updateItem(new CreateBeanRecord().setPayload("bean")); + StaticRecord staticRecord = staticTable.updateItem(new StaticRecord().setPayload("static")); + ImmutableRecord immutable = immutableTable.updateItem(ImmutableRecord.builder().payload("immutable").build()); + documentTable.putItem(EnhancedDocument.builder().putString("id", "doc-id").putString("payload", "doc").build()); + + software.amazon.awssdk.enhanced.dynamodb.model.BatchGetResultPageIterable pages = + enhancedClient.batchGetItem(r -> r.readBatches( + ReadBatch.builder(CreateBeanRecord.class) + .mappedTableResource(beanTable) + .addGetItem(i -> i.key(k -> k.partitionValue(bean.getId()))) + .build(), + ReadBatch.builder(StaticRecord.class) + .mappedTableResource(staticTable) + .addGetItem(i -> i.key(k -> k.partitionValue(staticRecord.getId()))) + .build(), + ReadBatch.builder(ImmutableRecord.class) + .mappedTableResource(immutableTable) + .addGetItem(i -> i.key(k -> k.partitionValue(immutable.id()))) + .build(), + ReadBatch.builder(EnhancedDocument.class) + .mappedTableResource(documentTable) + .addGetItem(i -> i.key(k -> k.partitionValue("doc-id"))) + .build())); + + List beans = + pages.resultsForTable(beanTable).stream().collect(Collectors.toList()); + List staticItems = + pages.resultsForTable(staticTable).stream().collect(Collectors.toList()); + List immutableItems = + pages.resultsForTable(immutableTable).stream().collect(Collectors.toList()); + List documents = + pages.resultsForTable(documentTable).stream().collect(Collectors.toList()); + + assertThat(beans).hasSize(1); + assertThat(staticItems).hasSize(1); + assertThat(immutableItems).hasSize(1); + assertThat(documents).hasSize(1); + assertThat(beans.get(0).getId()).isEqualTo(bean.getId()); + assertThat(documents.get(0).getString("payload")).isEqualTo("doc"); + } + + @Test + public void batchWrite_beanCreateAndDocument_generatesUuidAndPreservesDocument() { + DynamoDbEnhancedClient enhancedClient = enhancedClient(); + DynamoDbTable beanTable = + enhancedClient.table(newTableName("mixed-write-bean"), TableSchema.fromBean(CreateBeanRecord.class)); + DynamoDbTable documentTable = + enhancedClient.table(newTableName("mixed-write-document"), documentSchema()); + createTable(beanTable); + createTable(documentTable); + + enhancedClient.batchWriteItem(r -> r.addWriteBatch( + WriteBatch.builder(CreateBeanRecord.class) + .mappedTableResource(beanTable) + .addPutItem(new CreateBeanRecord().setPayload("bean")) + .build()) + .addWriteBatch( + WriteBatch.builder(EnhancedDocument.class) + .mappedTableResource(documentTable) + .addPutItem(EnhancedDocument.builder() + .putString("id", "doc-id") + .putString("payload", "doc") + .build()) + .build())); + + CreateBeanRecord bean = beanTable.scan().items().stream().findFirst().orElseThrow(IllegalStateException::new); + assertUuidGenerated(bean.getId()); + assertThat(documentTable.getItem(r -> r.key(k -> k.partitionValue("doc-id"))).getString("payload")).isEqualTo("doc"); + } + + @Test + public void transactGet_mixedBeanAndDocument_returnsItems() { + DynamoDbEnhancedClient enhancedClient = enhancedClient(); + DynamoDbTable beanTable = + enhancedClient.table(newTableName("mixed-tget-bean"), TableSchema.fromBean(CreateBeanRecord.class)); + DynamoDbTable documentTable = + enhancedClient.table(newTableName("mixed-tget-document"), documentSchema()); + createTable(beanTable); + createTable(documentTable); + + CreateBeanRecord bean = beanTable.updateItem(new CreateBeanRecord().setPayload("bean")); + documentTable.putItem(EnhancedDocument.builder().putString("id", "doc-id").putString("payload", "doc").build()); + + List results = enhancedClient.transactGetItems( + r -> r.addGetItem(beanTable, Key.builder().partitionValue(bean.getId()).build()) + .addGetItem(documentTable, Key.builder().partitionValue("doc-id").build())); + + assertThat(results).hasSize(2); + assertThat(results.get(0).getItem(beanTable).getId()).isEqualTo(bean.getId()); + assertThat(results.get(1).getItem(documentTable).getString("payload")).isEqualTo("doc"); + } + + @Test + public void transactWrite_mixedPutAndUpdate_createPreservesOnUpdate() { + DynamoDbEnhancedClient enhancedClient = enhancedClient(); + DynamoDbTable table = + enhancedClient.table(newTableName("mixed-twrite-create"), TableSchema.fromBean(CreateBeanRecord.class)); + createTable(table); + + CreateBeanRecord first = table.updateItem(new CreateBeanRecord().setPayload("first")); + enhancedClient.transactWriteItems( + r -> r.addPutItem(table, new CreateBeanRecord().setPayload("second")) + .addUpdateItem(table, new CreateBeanRecord().setId(first.getId()).setPayload("first-updated"))); + + CreateBeanRecord updated = table.getItem(r -> r.key(k -> k.partitionValue(first.getId()))); + CreateBeanRecord inserted = table.scan().items().stream() + .filter(item -> "second".equals(item.getPayload())) + .findFirst() + .orElseThrow(IllegalStateException::new); + + assertUuidPreserved(first.getId(), updated.getId()); + assertThat(updated.getPayload()).isEqualTo("first-updated"); + assertUuidGenerated(inserted.getId()); + } + + private static TableSchema documentSchema() { + return TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), "id", AttributeValueType.S) + .build(); + } + + private void createTable(DynamoDbTable table) { + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + createdTables.add(table.tableName()); + } + + private DynamoDbTable createTableResource(String tableName, TableSchema tableSchema) { + DynamoDbTable table = enhancedClient().table(tableName, tableSchema); + createTable(table); + return table; + } + + private void assertUuidGenerated(String value) { + assertThat(isValidUuid(value)).isTrue(); + } + + private void assertUuidPreserved(String before, String after) { + assertThat(after).isEqualTo(before); + } + + private void assertUuidRegenerated(String before, String after) { + assertThat(after).isNotEqualTo(before); + assertUuidGenerated(after); + } + + private void assertVersionIncremented(Integer previous, Integer current) { + assertThat(current).isEqualTo(previous + 1); + } + + private String newTableName(String prefix) { + return getConcreteTableName(prefix + "-" + UUID.randomUUID()); + } + + private static final TableSchema STATIC_CREATE_SCHEMA = + StaticTableSchema.builder(StaticRecord.class) + .newItemSupplier(StaticRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(StaticRecord::getId) + .setter(StaticRecord::setId) + .tags(primaryPartitionKey(), + autoGeneratedUuidAttribute(DynamoDbAutoGenerateStrategy.CREATE))) + .addAttribute(String.class, a -> a.name("payload") + .getter(StaticRecord::getPayload) + .setter(StaticRecord::setPayload)) + .build(); + + private static final TableSchema STATIC_IMMUTABLE_CREATE_SCHEMA = + StaticImmutableTableSchema.builder(StaticImmutableRecord.class, StaticImmutableRecord.Builder.class) + .newItemBuilder(StaticImmutableRecord::builder, StaticImmutableRecord.Builder::build) + .addAttribute(String.class, a -> a.name("id") + .getter(StaticImmutableRecord::id) + .setter(StaticImmutableRecord.Builder::id) + .tags(primaryPartitionKey(), + autoGeneratedUuidAttribute( + DynamoDbAutoGenerateStrategy.CREATE))) + .addAttribute(String.class, a -> a.name("payload") + .getter(StaticImmutableRecord::payload) + .setter(StaticImmutableRecord.Builder::payload)) + .build(); + + @DynamoDbBean + public static class CreateBeanRecord { + private String id; + private String payload; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + public String getId() { + return id; + } + + public CreateBeanRecord setId(String id) { + this.id = id; + return this; + } + + public String getPayload() { + return payload; + } + + public CreateBeanRecord setPayload(String payload) { + this.payload = payload; + return this; + } + } + + @DynamoDbBean + public static class AlwaysBeanRecord { + private String id; + private String payload; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid + public String getId() { + return id; + } + + public AlwaysBeanRecord setId(String id) { + this.id = id; + return this; + } + + public String getPayload() { + return payload; + } + + public AlwaysBeanRecord setPayload(String payload) { + this.payload = payload; + return this; + } + } + + @DynamoDbBean + public static class VersionedCreateBeanRecord { + private String id; + private String payload; + private Integer version; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + public String getId() { + return id; + } + + public VersionedCreateBeanRecord setId(String id) { + this.id = id; + return this; + } + + public String getPayload() { + return payload; + } + + public VersionedCreateBeanRecord setPayload(String payload) { + this.payload = payload; + return this; + } + + @DynamoDbVersionAttribute + public Integer getVersion() { + return version; + } + + public VersionedCreateBeanRecord setVersion(Integer version) { + this.version = version; + return this; + } + } + + @DynamoDbBean + public static class VersionedAlwaysBeanRecord { + private String id; + private String payload; + private Integer version; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid + public String getId() { + return id; + } + + public VersionedAlwaysBeanRecord setId(String id) { + this.id = id; + return this; + } + + public String getPayload() { + return payload; + } + + public VersionedAlwaysBeanRecord setPayload(String payload) { + this.payload = payload; + return this; + } + + @DynamoDbVersionAttribute + public Integer getVersion() { + return version; + } + + public VersionedAlwaysBeanRecord setVersion(Integer version) { + this.version = version; + return this; + } + } + + @DynamoDbBean + public static class StaticRecord { + private String id; + private String payload; + + public String getId() { + return id; + } + + public StaticRecord setId(String id) { + this.id = id; + return this; + } + + public String getPayload() { + return payload; + } + + public StaticRecord setPayload(String payload) { + this.payload = payload; + return this; + } + } + + @DynamoDbImmutable(builder = ImmutableRecord.Builder.class) + public static class ImmutableRecord { + private final String id; + private final String payload; + + private ImmutableRecord(Builder builder) { + this.id = builder.id; + this.payload = builder.payload; + } + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + public String id() { + return id; + } + + public String payload() { + return payload; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String id; + private String payload; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder payload(String payload) { + this.payload = payload; + return this; + } + + public ImmutableRecord build() { + return new ImmutableRecord(this); + } + } + } + + public static class StaticImmutableRecord { + private final String id; + private final String payload; + + private StaticImmutableRecord(Builder builder) { + this.id = builder.id; + this.payload = builder.payload; + } + + public String id() { + return id; + } + + public String payload() { + return payload; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String id; + private String payload; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder payload(String payload) { + this.payload = payload; + return this; + } + + public StaticImmutableRecord build() { + return new StaticImmutableRecord(this); + } + } + } + + @DynamoDbBean + public static class ConvertedBeanRecord { + private String id; + private ConvertedPayload payload; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid(strategy = DynamoDbAutoGenerateStrategy.CREATE) + public String getId() { + return id; + } + + public ConvertedBeanRecord setId(String id) { + this.id = id; + return this; + } + + @DynamoDbConvertedBy(ConvertedPayloadConverter.class) + public ConvertedPayload getPayload() { + return payload; + } + + public ConvertedBeanRecord setPayload(ConvertedPayload payload) { + this.payload = payload; + return this; + } + } + + public static class ConvertedPayload { + private final String value; + + public ConvertedPayload(String value) { + this.value = value; + } + + public String value() { + return value; + } + } + + public static class ConvertedPayloadConverter implements AttributeConverter { + @Override + public AttributeValue transformFrom(ConvertedPayload input) { + return input == null ? AttributeValue.builder().nul(true).build() : AttributeValue.fromS(input.value()); + } + + @Override + public ConvertedPayload transformTo(AttributeValue input) { + if (input == null || Boolean.TRUE.equals(input.nul())) { + return null; + } + return new ConvertedPayload(input.s()); + } + + @Override + public EnhancedType type() { + return EnhancedType.of(ConvertedPayload.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.S; + } + } +} 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 new file mode 100644 index 000000000000..458e31368d30 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java @@ -0,0 +1,324 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid; + +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.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; +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.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 AutoGeneratedUuidExtensionTest extends LocalDynamoDbSyncTestBase { + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(RecordWithAutogeneratedUuid.class); + + private final DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(Stream.concat( + ExtensionResolver.defaultExtensions().stream(), + Stream.of(AutoGeneratedUuidExtension.create())) + .collect(Collectors.toList())) + .build(); + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName("autogenerated-uuid-table"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("autogenerated-uuid-table"))); + } + + @Test + 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")); + + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); + } + + @Test + public void putItem_whenKeysAlreadyPopulated_replacesExistingUuids() { + RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + + mappedTable.putItem(record); + RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); + assertThat(result.getId()).isNotEqualTo("existing-id"); + assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); + } + + @Test + public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record1 = new RecordWithAutogeneratedUuid(); + RecordWithAutogeneratedUuid record2 = new RecordWithAutogeneratedUuid(); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(RecordWithAutogeneratedUuid.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build())); + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + + 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(); + } + + @Test + public void batchWrite_whenKeysAlreadyPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record1 = new RecordWithAutogeneratedUuid(); + record1.setId("existing-id-1"); + record1.setSortKey("existing-sk-1"); + + RecordWithAutogeneratedUuid record2 = new RecordWithAutogeneratedUuid(); + record2.setId("existing-id-2"); + record2.setSortKey("existing-sk-2"); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(RecordWithAutogeneratedUuid.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build())); + + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + + assertThat(results.size()).isEqualTo(2); + 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"); + assertThat(results.get(0).getSortKey()).isNotEqualTo("existing-sk-1"); + assertThat(results.get(1).getSortKey()).isNotEqualTo("existing-sk-2"); + } + + @Test + public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); + } + + @Test + public void transactWrite_whenKeysAlreadyPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); + assertThat(result.getId()).isNotEqualTo("existing-id"); + assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); + } + + @Test + public void putItem_whenAutogeneratedUuidAnnotationIsNotPresent_doesNotRegenerateUuids() { + String tableName = "no-autogenerated-uuid-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(RecordWithoutAutogeneratedUuid.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + RecordWithoutAutogeneratedUuid record = new RecordWithoutAutogeneratedUuid(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + record.setGsiPk("existing-gsiPk"); + record.setGsiSk("existing-gsiSk"); + record.setData("data"); + + 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"); + assertThat(retrieved.getGsiSk()).isEqualTo("existing-gsiSk"); + assertThat(retrieved.getData()).isEqualTo("data"); + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void createBean_whenAutogeneratedUuidAnnotationIsAppliedOnNonStringAttribute_throwsException() { + assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedUuidInvalidTypeRecord.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 Uuid attribute. Only String Class type is supported."); + } + + @DynamoDbBean + public static class RecordWithAutogeneratedUuid { + private String id; + private String sortKey; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + @DynamoDbAutoGeneratedUuid + public String getSortKey() { + return sortKey; + } + + public void setSortKey(String sortKey) { + this.sortKey = sortKey; + } + } + + @DynamoDbBean + public static class RecordWithoutAutogeneratedUuid { + private String id; + private String sortKey; + private String gsiPk; + private String gsiSk; + private String data; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSortKey() { + return sortKey; + } + + public void setSortKey(String sortKey) { + this.sortKey = sortKey; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + public String getGsiPk() { + return gsiPk; + } + + public void setGsiPk(String gsiPk) { + this.gsiPk = gsiPk; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1") + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getGsiSk() { + return gsiSk; + } + + public void setGsiSk(String gsiSk) { + this.gsiSk = gsiSk; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + @DynamoDbBean + public static class AutogeneratedUuidInvalidTypeRecord { + private Integer id; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } +} \ No newline at end of file