From 7cec4a2e6be008f424dba515154e551f07b84f15 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Thu, 6 Nov 2025 09:41:11 +0200 Subject: [PATCH 01/14] Added support for DynamoDbAutoGeneratedKey annotation --- ...-AmazonDynamoDBEnhancedClient-cbcc2bb.json | 6 + .../extensions/AutoGeneratedKeyExtension.java | 219 +++++++++ .../AutoGeneratedUuidExtension.java | 30 ++ .../annotations/DynamoDbAutoGeneratedKey.java | 77 ++++ .../extensions/AutoGeneratedKeyTag.java | 33 ++ .../enhanced/dynamodb/UuidTestUtils.java | 33 ++ .../AutoGeneratedKeyExtensionTest.java | 434 ++++++++++++++++++ .../AutoGeneratedUuidExtensionTest.java | 39 ++ .../ConflictingAnnotationsTest.java | 173 +++++++ .../AutoGeneratedKeyRecordTest.java | 410 +++++++++++++++++ .../AutoGeneratedUuidRecordTest.java | 60 ++- 11 files changed, 1509 insertions(+), 5 deletions(-) create mode 100644 .changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedKeyTag.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/UuidTestUtils.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json new file mode 100644 index 000000000000..b8c7700af796 --- /dev/null +++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon DynamoDB Enhanced Client", + "contributor": "", + "description": "Added the support for DynamoDbAutoGeneratedKey annotation" +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java new file mode 100644 index 000000000000..0898bb7e57d1 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java @@ -0,0 +1,219 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.extensions; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.StringUtils; +import software.amazon.awssdk.utils.Validate; + +/** + * Generates a random UUID (via {@link java.util.UUID#randomUUID()}) for any attribute tagged with + * {@code @DynamoDbAutoGeneratedKey} when that attribute is missing or empty on a write (put/update). + *

+ * Key Difference from @DynamoDbAutoGeneratedUuid: This extension only generates UUIDs when the + * attribute value is null or empty, preserving existing values. In contrast, {@code @DynamoDbAutoGeneratedUuid} always generates + * new UUIDs regardless of existing values. + *

+ * Conflict Detection: This extension cannot be used together with {@code @DynamoDbAutoGeneratedUuid} on the same + * attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown at runtime to + * prevent unpredictable behavior based on extension load order. + *

+ * The annotation may be placed only on key attributes: + *

+ * + *

Validation: The extension enforces this at runtime during {@link #beforeWrite} by comparing the + * annotated attributes against the table's known key attributes. If an annotated attribute + * is not a PK/SK or an GSI/LSI, an {@link IllegalArgumentException} is thrown.

+ * + *

UpdateBehavior Limitations: {@code @DynamoDbUpdateBehavior} has no effect on primary keys due to + * DynamoDB's UpdateItem API requirements. It only affects secondary index keys.

+ */ +@SdkPublicApi +@ThreadSafe +public final class AutoGeneratedKeyExtension implements DynamoDbEnhancedClientExtension { + + /** + * Custom metadata key under which we store the set of annotated attribute names. + */ + private static final String CUSTOM_METADATA_KEY = + "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute"; + + /** + * Metadata key used by AutoGeneratedUuidExtension to detect conflicts. + */ + private static final String UUID_EXTENSION_METADATA_KEY = + "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; + + private static final AutoGeneratedKeyAttribute AUTO_GENERATED_KEY_ATTRIBUTE = new AutoGeneratedKeyAttribute(); + + private AutoGeneratedKeyExtension() { + } + + public static Builder builder() { + return new Builder(); + } + + /** + * If this table has attributes tagged for auto-generation, insert a UUID value into the outgoing item for any such attribute + * that is currently missing/empty. Unlike {@code @DynamoDbAutoGeneratedUuid}, this preserves existing values. + *

+ * Also validates that the annotation is only used on PK/SK/GSI/LSI key attributes and that there are no conflicts with + * + * @DynamoDbAutoGeneratedUuid. + */ + @Override + public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) { + Collection taggedAttributes = context.tableMetadata() + .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class) + .orElse(null); + + if (taggedAttributes == null || taggedAttributes.isEmpty()) { + return WriteModification.builder().build(); + } + + // Check for conflicts with @DynamoDbAutoGeneratedUuid + Collection uuidTaggedAttributes = context.tableMetadata() + .customMetadataObject(UUID_EXTENSION_METADATA_KEY, Collection.class) + .orElse(Collections.emptyList()); + + taggedAttributes.stream() + .filter(uuidTaggedAttributes::contains) + .findFirst() + .ifPresent(attribute -> { + throw new IllegalArgumentException( + "Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and " + + "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors " + + "and cannot be used together on the same attribute."); + }); + + TableMetadata meta = context.tableMetadata(); + Set allowedKeys = new HashSet<>(); + + // ensure every @DynamoDbAutoGeneratedKey attribute is a PK/SK or GSI/LSI. If not, throw IllegalArgumentException + allowedKeys.add(meta.primaryPartitionKey()); + meta.primarySortKey().ifPresent(allowedKeys::add); + + for (IndexMetadata idx : meta.indices()) { + String indexName = idx.name(); + allowedKeys.add(meta.indexPartitionKey(indexName)); + meta.indexSortKey(indexName).ifPresent(allowedKeys::add); + } + + taggedAttributes.stream() + .filter(attr -> !allowedKeys.contains(attr)) + .findFirst() + .ifPresent(attr -> { + throw new IllegalArgumentException( + "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " + + "primary partition key, primary sort key, or GSI/LSI partition/sort keys." + + "Invalid placement on attribute: " + attr); + }); + + // Generate UUIDs for missing/empty annotated attributes + Map itemToTransform = new HashMap<>(context.items()); + taggedAttributes.forEach(attr -> insertUuidIfMissing(itemToTransform, attr)); + + return WriteModification.builder() + .transformedItem(Collections.unmodifiableMap(itemToTransform)) + .build(); + } + + private void insertUuidIfMissing(Map itemToTransform, String key) { + AttributeValue existing = itemToTransform.get(key); + if (Objects.isNull(existing) || StringUtils.isBlank(existing.s())) { + itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build()); + } + } + + /** + * Static helpers used by the {@code @BeanTableSchemaAttributeTag}-based annotation tag. + */ + public static final class AttributeTags { + private AttributeTags() { + } + + /** + * @return a {@link StaticAttributeTag} that marks the attribute for auto-generated key behavior. + */ + public static StaticAttributeTag autoGeneratedKeyAttribute() { + return AUTO_GENERATED_KEY_ATTRIBUTE; + } + } + + /** + * Stateless builder. + */ + public static final class Builder { + private Builder() { + } + + public AutoGeneratedKeyExtension build() { + return new AutoGeneratedKeyExtension(); + } + } + + /** + * Validates Java type and records the tagged attribute into table metadata so {@link #beforeWrite} can find it at runtime. + */ + private static final class AutoGeneratedKeyAttribute implements StaticAttributeTag { + + @Override + public void validateType(String attributeName, + EnhancedType type, + AttributeValueType attributeValueType) { + + Validate.notNull(type, "type is null"); + Validate.notNull(type.rawClass(), "rawClass is null"); + Validate.notNull(attributeValueType, "attributeValueType is null"); + + if (!type.rawClass().equals(String.class)) { + throw new IllegalArgumentException(String.format( + "Attribute '%s' of Class type %s is not a suitable Java Class type to be used as a Auto Generated " + + "Key attribute. Only String Class type is supported.", attributeName, type.rawClass())); + } + } + + @Override + public Consumer modifyMetadata(String attributeName, + AttributeValueType attributeValueType) { + // Record the names of the attributes annotated with @DynamoDbAutoGeneratedKey for later lookup in beforeWrite() + return metadata -> metadata.addCustomMetadataObject( + CUSTOM_METADATA_KEY, Collections.singleton(attributeName)); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java index d92db8c60bbd..5a4712a177d3 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 @@ -39,6 +39,14 @@ * every time a new record is written to the database. The generated UUID is obtained using the * {@link java.util.UUID#randomUUID()} method. *

+ * Key Difference from @DynamoDbAutoGeneratedKey: This extension always generates new UUIDs on every write, + * regardless of existing values. In contrast, {@code @DynamoDbAutoGeneratedKey} only generates UUIDs when the + * attribute value is null or empty, preserving existing values. + *

+ * Conflict Detection: This extension cannot be used together with {@code @DynamoDbAutoGeneratedKey} on the same + * attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown + * at runtime to prevent unpredictable behavior. + *

* 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 * extension when creating the enhanced client. @@ -79,6 +87,13 @@ public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientExtension { private static final String CUSTOM_METADATA_KEY = "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; + + /** + * Metadata key used by AutoGeneratedKeyExtension to detect conflicts. + */ + private static final String KEY_EXTENSION_METADATA_KEY = + "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute"; + private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute(); private AutoGeneratedUuidExtension() { @@ -109,6 +124,21 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex return WriteModification.builder().build(); } + // Check for conflicts with @DynamoDbAutoGeneratedKey + Collection keyTaggedAttributes = context.tableMetadata() + .customMetadataObject(KEY_EXTENSION_METADATA_KEY, Collection.class) + .orElse(Collections.emptyList()); + + customMetadataObject.stream() + .filter(keyTaggedAttributes::contains) + .findFirst() + .ifPresent(attribute -> { + throw new IllegalArgumentException( + "Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and " + + "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors " + + "and cannot be used together on the same attribute."); + }); + Map itemToTransform = new HashMap<>(context.items()); customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key)); return WriteModification.builder() diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java new file mode 100644 index 000000000000..3fe249b5eb0f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java @@ -0,0 +1,77 @@ +/* + * 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 java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AutoGeneratedKeyTag; +import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; + +/** + * Annotation that marks a key attribute to be automatically populated with a random UUID if no value is provided during a write + * operation (put or update). This annotation is intended to work specifically with key attributes. + * + *

This annotation is designed for use with the V2 {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}. + * It is registered via {@link BeanTableSchemaAttributeTag} and its behavior is implemented by + * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension}.

+ * + *

Where this annotation can be applied

+ * This annotation is only valid on attributes that serve as keys: + * + * If applied to any other attribute, the {@code AutoGeneratedKeyExtension} will throw an + * {@link IllegalArgumentException} at runtime. + * + *

How values are generated

+ * + * + *

Behavior with UpdateBehavior

+ *

Primary Keys: {@link DynamoDbUpdateBehavior} has no effect on primary partition keys + * or primary sort keys. Primary keys are required for UpdateItem operations in DynamoDB and cannot be conditionally + * updated. UUIDs will be generated whenever the primary key attribute is missing or empty, regardless of any + * {@code UpdateBehavior} setting.

+ * + *

Secondary Index Keys: For GSI/LSI keys, {@link DynamoDbUpdateBehavior} can be used: + *

+ * + *

Type restriction

+ * This annotation is only valid on attributes of type {@link String}. + */ +@SdkPublicApi +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target( {ElementType.METHOD, ElementType.FIELD}) +@BeanTableSchemaAttributeTag(AutoGeneratedKeyTag.class) +public @interface DynamoDbAutoGeneratedKey { +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedKeyTag.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedKeyTag.java new file mode 100644 index 000000000000..815e15bccd7a --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/AutoGeneratedKeyTag.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.internal.extensions; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag; + +@SdkInternalApi +public final class AutoGeneratedKeyTag { + + private AutoGeneratedKeyTag() { + } + + public static StaticAttributeTag attributeTagFor(DynamoDbAutoGeneratedKey annotation) { + return AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute(); + } + +} \ 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/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java new file mode 100644 index 000000000000..ee053f1a16fe --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java @@ -0,0 +1,434 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + */ + +package software.amazon.awssdk.enhanced.dynamodb.extensions; + +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.AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute; +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.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; + +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +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.internal.extensions.DefaultDynamoDbExtensionContext; +import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext; +import software.amazon.awssdk.enhanced.dynamodb.internal.operations.OperationName; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class AutoGeneratedKeyExtensionTest { + + private static final String RECORD_ID = "id123"; + private static final String TABLE_NAME = "table-name"; + + private static final OperationContext PRIMARY_CONTEXT = + DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName()); + + private final AutoGeneratedKeyExtension extension = AutoGeneratedKeyExtension.builder().build(); + + /** + * Schema that places @DynamoDbAutoGeneratedKey on GSI key ("keyAttribute") so the validation passes. + */ + private static final StaticTableSchema ITEM_WITH_KEY_SCHEMA = + StaticTableSchema.builder(ItemWithKey.class) + .newItemSupplier(ItemWithKey::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithKey::getId) + .setter(ItemWithKey::setId) + .addTag(primaryPartitionKey()) // PK + .addTag(autoGeneratedKeyAttribute())) + .addAttribute(String.class, a -> a.name("keyAttribute") + .getter(ItemWithKey::getKeyAttribute) + .setter(ItemWithKey::setKeyAttribute) + .tags( + secondaryPartitionKey("gsi_keys_only"), // GSI + autoGeneratedKeyAttribute())) + .addAttribute(String.class, a -> a.name("simpleString") + .getter(ItemWithKey::getSimpleString) + .setter(ItemWithKey::setSimpleString)) + .build(); + + /** + * Schema that places @DynamoDbAutoGeneratedKey on a NON-KEY attribute to trigger the exception. + */ + private static final StaticTableSchema INVALID_NONKEY_AUTOGEN_SCHEMA = + StaticTableSchema.builder(ItemWithKey.class) + .newItemSupplier(ItemWithKey::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithKey::getId) + .setter(ItemWithKey::setId) + .addTag(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("keyAttribute") + .getter(ItemWithKey::getKeyAttribute) + .setter(ItemWithKey::setKeyAttribute) + // No index tags here — autogen on non-key fails at beforeWrite() + .addTag(autoGeneratedKeyAttribute())) + .addAttribute(String.class, a -> a.name("simpleString") + .getter(ItemWithKey::getSimpleString) + .setter(ItemWithKey::setSimpleString)) + .build(); + + /** + * Schema that places @DynamoDbAutoGeneratedKey on LSI key ("simpleString") so the validation passes. + */ + private static final StaticTableSchema LSI_SK_AUTOGEN_SCHEMA = + StaticTableSchema.builder(ItemWithKey.class) + .newItemSupplier(ItemWithKey::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithKey::getId) + .setter(ItemWithKey::setId) + .addTag(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("keyAttribute") + .getter(ItemWithKey::getKeyAttribute) + .setter(ItemWithKey::setKeyAttribute)) + .addAttribute(String.class, a -> a.name("simpleString") + .getter(ItemWithKey::getSimpleString) + .setter(ItemWithKey::setSimpleString) + .tags( + secondarySortKey("lsi1"), // LSI + autoGeneratedKeyAttribute())) + .build(); + + @Test + public void updateItem_withExistingKey_preservesValueAndDoesNotGenerateNew() { + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); + String preset = UUID.randomUUID().toString(); + item.setKeyAttribute(preset); + + Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); + assertThat(items).hasSize(2); + + WriteModification result = + extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) + .operationName(OperationName.UPDATE_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + Map transformed = result.transformedItem(); + assertThat(transformed).isNotNull().hasSize(2); + assertThat(transformed).containsEntry("id", AttributeValue.fromS(RECORD_ID)); + + // Ensures the attribute remains a valid UUID without altering the preset value + assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue(); + assertThat(result.updateExpression()).isNull(); + } + + @Test + public void updateItem_withoutKey_generatesNewUuid() { + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); + + Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); + assertThat(items).hasSize(1); + + WriteModification result = + extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) + .operationName(OperationName.UPDATE_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + Map transformed = result.transformedItem(); + assertThat(transformed).isNotNull().hasSize(2); + assertThat(transformed).containsEntry("id", AttributeValue.fromS(RECORD_ID)); + assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue(); + assertThat(result.updateExpression()).isNull(); + } + + @Test + public void updateItem_withMissingKeyAttribute_insertsGeneratedUuid() { + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); + + Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); + assertThat(items).hasSize(1); + + WriteModification result = + extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) + .operationName(OperationName.UPDATE_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + assertThat(result.transformedItem()).isNotNull(); + assertThat(result.updateExpression()).isNull(); + assertThat(result.transformedItem()).hasSize(2); + assertThat(isValidUuid(result.transformedItem().get("keyAttribute").s())).isTrue(); + } + + @Test + public void nonStringTypeAnnotatedWithAutoGeneratedKey_throwsIllegalArgumentException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + StaticTableSchema.builder(ItemWithKey.class) + .newItemSupplier(ItemWithKey::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithKey::getId) + .setter(ItemWithKey::setId) + .addTag(primaryPartitionKey())) + .addAttribute(Integer.class, a -> a.name("intAttribute") + .getter(ItemWithKey::getIntAttribute) + .setter(ItemWithKey::setIntAttribute) + .addTag(autoGeneratedKeyAttribute())) + .addAttribute(String.class, a -> a.name("simpleString") + .getter(ItemWithKey::getSimpleString) + .setter(ItemWithKey::setSimpleString)) + .build() + ) + .withMessage("Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type " + + "to be used as a Auto Generated Key attribute. Only String Class type is supported."); + } + + @Test + public void autoGeneratedKey_onSecondaryPartitionKey_generatesUuid() { + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); // keyAttribute (GSI PK) is missing → should be generated + + Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); + + WriteModification result = + extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + Map transformed = result.transformedItem(); + assertThat(transformed).isNotNull(); + assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue(); // generated for GSI PK + } + + @Test + public void autoGeneratedKey_onSecondarySortKey_generatesUuid() { + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); // simpleString (GSI/LSI) is missing → should be generated + + Map items = LSI_SK_AUTOGEN_SCHEMA.itemToMap(item, true); + + WriteModification result = + extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(LSI_SK_AUTOGEN_SCHEMA.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + Map transformed = result.transformedItem(); + assertThat(transformed).isNotNull(); + assertThat(isValidUuid(transformed.get("simpleString").s())).isTrue(); // generated for index SK + } + + @Test + public void autoGeneratedKey_onNonKey_throwsIllegalArgumentException() { + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); // keyAttribute is annotated but NOT a key in this schema → should fail at beforeWrite() + + Map items = INVALID_NONKEY_AUTOGEN_SCHEMA.itemToMap(item, true); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(INVALID_NONKEY_AUTOGEN_SCHEMA.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()) + ) + .withMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes: " + + "primary partition key, primary sort key, or GSI/LSI partition/sort keys.") + .withMessageContaining("keyAttribute"); + } + + @Test + public void conflictingAnnotations_throwsIllegalArgumentException() { + // Create a schema with both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid on the same attribute + StaticTableSchema conflictingSchema = + StaticTableSchema + .builder(ItemWithKey.class) + .newItemSupplier(ItemWithKey::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithKey::getId) + .setter(ItemWithKey::setId) + .addTag(primaryPartitionKey()) + // Both annotations on the same attribute + .addTag(autoGeneratedKeyAttribute()) + .addTag(autoGeneratedUuidAttribute())) + .addAttribute(String.class, a -> a.name("simpleString") + .getter(ItemWithKey::getSimpleString) + .setter(ItemWithKey::setSimpleString)) + .build(); + + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); + + Map items = conflictingSchema.itemToMap(item, true); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(conflictingSchema.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()) + ) + .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + } + + @Test + public void conflictingAnnotations_onSecondaryKey_throwsIllegalArgumentException() { + // Create a schema with both annotations on a GSI key + StaticTableSchema conflictingGsiSchema = + StaticTableSchema + .builder(ItemWithKey.class) + .newItemSupplier(ItemWithKey::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithKey::getId) + .setter(ItemWithKey::setId) + .addTag(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("keyAttribute") + .getter(ItemWithKey::getKeyAttribute) + .setter(ItemWithKey::setKeyAttribute) + .addTag(secondaryPartitionKey("gsi1")) + // Both annotations on the same GSI key + .addTag(autoGeneratedKeyAttribute()) + .addTag(autoGeneratedUuidAttribute())) + .addAttribute(String.class, a -> a.name("simpleString") + .getter(ItemWithKey::getSimpleString) + .setter(ItemWithKey::setSimpleString)) + .build(); + + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); + + Map items = conflictingGsiSchema.itemToMap(item, true); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(conflictingGsiSchema.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build())) + .withMessage("Attribute 'keyAttribute' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " + + "annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + } + + @Test + public void conflictDetection_worksRegardlessOfExtensionOrder() { + // Verify that AutoGeneratedKeyExtension detects conflicts even when + // AutoGeneratedUuidExtension metadata is already present + StaticTableSchema conflictingSchema = + StaticTableSchema + .builder(ItemWithKey.class) + .newItemSupplier(ItemWithKey::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithKey::getId) + .setter(ItemWithKey::setId) + .addTag(primaryPartitionKey()) + .addTag(autoGeneratedKeyAttribute()) + .addTag(autoGeneratedUuidAttribute())) + .build(); + + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); + + Map items = conflictingSchema.itemToMap(item, true); + + // Test that the conflict is detected regardless of which extension runs first + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(conflictingSchema.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build())) + .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " + + "annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + } + + private static class ItemWithKey { + + private String id; + private String keyAttribute; + private String simpleString; + private Integer intAttribute; + + ItemWithKey() { + } + + public Integer getIntAttribute() { + return intAttribute; + } + + public void setIntAttribute(Integer intAttribute) { + this.intAttribute = intAttribute; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getKeyAttribute() { + return keyAttribute; + } + + public void setKeyAttribute(String keyAttribute) { + this.keyAttribute = keyAttribute; + } + + public String getSimpleString() { + return simpleString; + } + + public void setSimpleString(String simpleString) { + this.simpleString = simpleString; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ItemWithKey)) { + return false; + } + ItemWithKey that = (ItemWithKey) o; + return Objects.equals(id, that.id) + && Objects.equals(keyAttribute, that.keyAttribute) + && Objects.equals(simpleString, that.simpleString); + } + + @Override + public int hashCode() { + return Objects.hash(id, keyAttribute, simpleString); + } + } +} 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 cc69f503d50f..cd92fa5fb063 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 @@ -156,6 +156,45 @@ void IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() { + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported."); } + @Test + public void conflictingAnnotations_throwsIllegalArgumentException() { + // Create a schema with both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid on the same attribute + StaticTableSchema conflictingSchema = + StaticTableSchema.builder(ItemWithUuid.class) + .newItemSupplier(ItemWithUuid::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(ItemWithUuid::getId) + .setter(ItemWithUuid::setId) + .addTag(primaryPartitionKey()) + // Both annotations on the same attribute + .addTag(AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute()) + .addTag(AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute())) + .addAttribute(String.class, + a -> a.name("simpleString") + .getter(ItemWithUuid::getSimpleString) + .setter(ItemWithUuid::setSimpleString)) + .build(); + + ItemWithUuid item = new ItemWithUuid(); + item.setId(RECORD_ID); + + Map items = conflictingSchema.itemToMap(item, true); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + atomicCounterExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(conflictingSchema.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()) + ) + .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + } + 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/extensions/ConflictingAnnotationsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java new file mode 100644 index 000000000000..c2ed794ee310 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java @@ -0,0 +1,173 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.extensions; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; + +import java.util.Map; +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.internal.extensions.DefaultDynamoDbExtensionContext; +import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext; +import software.amazon.awssdk.enhanced.dynamodb.internal.operations.OperationName; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Tests to verify that conflicting annotations (@DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid) are properly detected + * and throw exceptions regardless of extension load order. + */ +public class ConflictingAnnotationsTest { + + private static final String RECORD_ID = "test-id"; + private static final String TABLE_NAME = "test-table"; + private static final OperationContext PRIMARY_CONTEXT = + DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName()); + + private final AutoGeneratedKeyExtension keyExtension = AutoGeneratedKeyExtension.builder().build(); + private final AutoGeneratedUuidExtension uuidExtension = AutoGeneratedUuidExtension.create(); + + /** + * Schema with both annotations on the same attribute to test conflict detection. + */ + private static final StaticTableSchema CONFLICTING_SCHEMA = + StaticTableSchema.builder(TestItem.class) + .newItemSupplier(TestItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(TestItem::getId) + .setter(TestItem::setId) + .addTag(primaryPartitionKey()) + // Both annotations on the same attribute + .addTag(autoGeneratedKeyAttribute()) + .addTag(autoGeneratedUuidAttribute())) + .build(); + + @Test + public void keyExtensionFirst_detectsConflictWithUuidExtension() { + TestItem item = new TestItem(); + item.setId(RECORD_ID); + + Map items = CONFLICTING_SCHEMA.itemToMap(item, true); + + // AutoGeneratedKeyExtension runs first and detects conflict + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> keyExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(CONFLICTING_SCHEMA.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()) + ) + .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + } + + @Test + public void uuidExtensionFirst_detectsConflictWithKeyExtension() { + TestItem item = new TestItem(); + item.setId(RECORD_ID); + + Map items = CONFLICTING_SCHEMA.itemToMap(item, true); + + // AutoGeneratedUuidExtension runs first and detects conflict + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> uuidExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(CONFLICTING_SCHEMA.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()) + ) + .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + } + + @Test + public void separateAttributes_noConflict() { + // Schema with annotations on different attributes - should work fine + StaticTableSchema separateSchema = + StaticTableSchema.builder(TestItemSeparate.class) + .newItemSupplier(TestItemSeparate::new) + .addAttribute(String.class, a -> a.name("id") + .getter(TestItemSeparate::getId) + .setter(TestItemSeparate::setId) + .addTag(primaryPartitionKey()) + .addTag(autoGeneratedKeyAttribute())) + .addAttribute(String.class, a -> a.name("uuidField") + .getter(TestItemSeparate::getUuidField) + .setter(TestItemSeparate::setUuidField) + .addTag(autoGeneratedUuidAttribute())) + .build(); + + TestItemSeparate item = new TestItemSeparate(); + + Map items = separateSchema.itemToMap(item, true); + + // Both extensions should work without conflict when on different attributes + keyExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(separateSchema.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + uuidExtension.beforeWrite(DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(separateSchema.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + } + + public static class TestItem { + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } + + public static class TestItemSeparate { + private String id; + private String uuidField; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUuidField() { + return uuidField; + } + + public void setUuidField(String uuidField) { + this.uuidField = uuidField; + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java new file mode 100644 index 000000000000..7416fd92e5d2 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java @@ -0,0 +1,410 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.AutoGeneratedUuidRecordTest.assertValidUuid; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; + +import java.util.Arrays; +import java.util.Collection; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; +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.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.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; + +/** + * Tests for @DynamoDbAutoGeneratedKey annotation functionality. + *

+ * Tests cover: - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK) - UpdateBehavior control (WRITE_ALWAYS vs + * WRITE_IF_NOT_EXISTS) for secondary index keys - Primary key limitations (UpdateBehavior has no effect) - Error handling for + * invalid usage - Integration with other extensions (VersionedRecord) + */ +@RunWith(Parameterized.class) +public class AutoGeneratedKeyRecordTest extends LocalDynamoDbSyncTestBase { + + private final DynamoDbTable mappedTable; + + public AutoGeneratedKeyRecordTest(String testName, TableSchema schema) { + this.mappedTable = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedKeyExtension.builder().build()) + .build() + .table(getConcreteTableName("AutoGenKey-table"), schema); + } + + @Parameters(name = "{index}: {0}") + public static Collection data() { + return Arrays.asList(new Object[][] { + {"StaticTableSchema", createStaticSchema()}, + {"BeanTableSchema", TableSchema.fromBean(TestRecord.class)} + }); + } + + private static TableSchema createStaticSchema() { + return StaticTableSchema.builder(TestRecord.class) + .newItemSupplier(TestRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(TestRecord::getId) + .setter(TestRecord::setId) + .tags(primaryPartitionKey(), + AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute())) + .addAttribute(String.class, a -> a.name("sortKey") + .getter(TestRecord::getSortKey) + .setter(TestRecord::setSortKey) + .tags(primarySortKey(), + AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute())) + .addAttribute(String.class, a -> a.name("gsiPk") + .getter(TestRecord::getGsiPk) + .setter(TestRecord::setGsiPk) + .tags(secondaryPartitionKey("gsi1"), + AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute())) + .addAttribute(String.class, a -> a.name("gsiSk") + .getter(TestRecord::getGsiSk) + .setter(TestRecord::setGsiSk) + .tags(secondarySortKey("gsi1"), + AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute(), + updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(String.class, a -> a.name("payload") + .getter(TestRecord::getPayload) + .setter(TestRecord::setPayload)) + .build(); + } + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + mappedTable.deleteTable(); + } + + @Test + public void putItem_generatesUuidsForAllFourKeyTypes() { + TestRecord record = new TestRecord(); + // Don't set any keys - they should all be auto-generated + mappedTable.putItem(record); + + TestRecord retrieved = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + // Verify all 4 key types are generated + assertValidUuid(retrieved.getId()); // Primary partition key + assertValidUuid(retrieved.getSortKey()); // Primary sort key + assertValidUuid(retrieved.getGsiPk()); // GSI partition key + assertValidUuid(retrieved.getGsiSk()); // GSI sort key + + // Verify they're all different + assertThat(retrieved.getId()).isNotEqualTo(retrieved.getSortKey()); + assertThat(retrieved.getGsiPk()).isNotEqualTo(retrieved.getGsiSk()); + assertThat(retrieved.getId()).isNotEqualTo(retrieved.getGsiPk()); + } + + @Test + public void updateItem_respectsUpdateBehaviorForSecondaryIndexKeys() { + // Put initial record + TestRecord record = new TestRecord(); + mappedTable.putItem(record); + + TestRecord afterPut = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + String id = afterPut.getId(); + String sortKey = afterPut.getSortKey(); + String originalGsiPk = afterPut.getGsiPk(); // WRITE_ALWAYS (default) → should change + String originalGsiSk = afterPut.getGsiSk(); // WRITE_IF_NOT_EXISTS → should preserve + + // Update record + TestRecord updateRecord = new TestRecord(); + updateRecord.setId(id); + updateRecord.setSortKey(sortKey); + updateRecord.setPayload("updated"); + mappedTable.updateItem(updateRecord); + + TestRecord afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue(id).sortValue(sortKey))); + assertThat(afterUpdate.getGsiPk()).isNotEqualTo(originalGsiPk); // Regenerated + assertThat(afterUpdate.getGsiSk()).isEqualTo(originalGsiSk); // Preserved + assertEquals("updated", afterUpdate.getPayload()); + } + + @Test + public void nonKeyAttribute_throwsException() { + String tableName = getConcreteTableName("invalid-usage-test"); + DynamoDbEnhancedClient client = createClient(); + + TableSchema schema = StaticTableSchema.builder(InvalidUsageRecord.class) + .newItemSupplier(InvalidUsageRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(InvalidUsageRecord::getId) + .setter(InvalidUsageRecord::setId) + .addTag(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("notAKey") + .getter(InvalidUsageRecord::getNotAKey) + .setter(InvalidUsageRecord::setNotAKey) + .addTag(AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute())) + .build(); + + DynamoDbTable table = client.table(tableName, schema); + + try { + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + InvalidUsageRecord record = new InvalidUsageRecord(); + record.setId("test-id"); + + Assertions.assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> table.putItem(record)) + .withMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes") + .withMessageContaining("notAKey"); + } finally { + deleteTableByName(tableName); + } + } + + @Test + public void versionedRecord_worksWithAutoGeneratedKeys() { + // NOTE: UpdateBehavior has no effect on primary keys since they cannot be null in DynamoDB + String tableName = getConcreteTableName("versioned-test"); + DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedKeyExtension.builder().build(), + VersionedRecordExtension.builder().build()) + .build(); + + DynamoDbTable table = client.table(tableName, TableSchema.fromBean(VersionedRecord.class)); + + try { + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + VersionedRecord record = new VersionedRecord(); + record.setPayload("initial"); + table.putItem(record); + + VersionedRecord retrieved = table.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertValidUuid(retrieved.getId()); + assertEquals("initial", retrieved.getPayload()); + assertThat(retrieved.getVersion()).isEqualTo(1L); + + // Update to test versioning still works + retrieved.setPayload("updated"); + VersionedRecord updated = table.updateItem(retrieved); + assertEquals("updated", updated.getPayload()); + assertThat(updated.getVersion()).isEqualTo(2L); + } finally { + deleteTableByName(tableName); + } + } + + @Test + public void conflictingAnnotations_throwsException() { + String tableName = getConcreteTableName("conflicting-annotations-test"); + DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedKeyExtension.builder().build(), + AutoGeneratedUuidExtension.create()) + .build(); + + try { + DynamoDbTable table = + client.table(tableName, TableSchema.fromBean(ConflictingAnnotationsRecord.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + ConflictingAnnotationsRecord record = new ConflictingAnnotationsRecord(); + record.setPayload("test"); + + Assertions + .assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> table.putItem(record)) + .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " + + "annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + } finally { + deleteTableByName(tableName); + } + } + + + private DynamoDbEnhancedClient createClient() { + return DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedKeyExtension.builder().build()) + .build(); + } + + private void deleteTableByName(String tableName) { + getDynamoDbClient().deleteTable(b -> b.tableName(tableName)); + } + + @DynamoDbBean + public static class TestRecord { + private String id; + private String sortKey; + private String gsiPk; + private String gsiSk; + private String payload; + + // NOTE: UpdateBehavior has no effect on primary keys since they cannot be null in DynamoDB + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + // NOTE: UpdateBehavior has no effect on primary keys since they cannot be null in DynamoDB + @DynamoDbSortKey + @DynamoDbAutoGeneratedKey + public String getSortKey() { + return sortKey; + } + + public void setSortKey(String sortKey) { + this.sortKey = sortKey; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + public String getGsiPk() { + return gsiPk; + } + + public void setGsiPk(String gsiPk) { + this.gsiPk = gsiPk; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbSecondarySortKey(indexNames = "gsi1") + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getGsiSk() { + return gsiSk; + } + + public void setGsiSk(String gsiSk) { + this.gsiSk = gsiSk; + } + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + } + + public static class InvalidUsageRecord { + private String id; + private String notAKey; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getNotAKey() { + return notAKey; + } + + public void setNotAKey(String notAKey) { + this.notAKey = notAKey; + } + } + + @DynamoDbBean + public static class VersionedRecord { + private String id; + private Long version; + private String payload; + + // NOTE: UpdateBehavior has no effect on primary keys since they cannot be null in DynamoDB + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + } + + @DynamoDbBean + public static class ConflictingAnnotationsRecord { + private String id; + private String payload; + + // Both annotations on the same field - should cause an exception + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + @DynamoDbAutoGeneratedUuid + 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; + } + } +} \ No newline at end of file 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 e59ea214399b..3bdc7a5eb218 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 @@ -23,7 +23,6 @@ import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -45,7 +44,9 @@ import software.amazon.awssdk.enhanced.dynamodb.OperationContext; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; 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; @@ -55,13 +56,9 @@ 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.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{ @@ -314,6 +311,33 @@ public void updateItemConditionTestFailure() { } + @Test + public void conflictingAnnotations_throwsException() { + String tableName = getConcreteTableName("conflicting-annotations-test"); + DynamoDbEnhancedClient client = + DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedUuidExtension.create(), AutoGeneratedKeyExtension.builder().build()) + .build(); + + try { + DynamoDbTable table = + client.table(tableName, TableSchema.fromBean(ConflictingAnnotationsRecord.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + ConflictingAnnotationsRecord record = new ConflictingAnnotationsRecord(); + record.setPayload("test"); + + Assertions.assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> table.putItem(record)) + .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " + + "annotations. These annotations have conflicting behaviors and cannot be used together " + + "on the same attribute."); + } finally { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder().tableName(tableName).build()); + } + } + public static Record createUniqueFakeItem() { Record record = new Record(); record.setId(UUID.randomUUID().toString()); @@ -476,4 +500,30 @@ public String toString() { '}'; } } + + @DynamoDbBean + public static class ConflictingAnnotationsRecord { + private String id; + private String payload; + + // Both annotations on the same field - should cause an exception + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid + @DynamoDbAutoGeneratedKey + 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; + } + } } From d61560503b0496f909eb573df790907b0060ebf0 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 14 Nov 2025 10:58:42 +0200 Subject: [PATCH 02/14] Added support for DynamoDbAutoGeneratedKey annotation --- .../annotations/DynamoDbAutoGeneratedKey.java | 6 +- .../AutoGeneratedKeyRecordTest.java | 124 ++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java index 3fe249b5eb0f..7a4c7ce90df5 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java @@ -54,9 +54,9 @@ * *

Behavior with UpdateBehavior

*

Primary Keys: {@link DynamoDbUpdateBehavior} has no effect on primary partition keys - * or primary sort keys. Primary keys are required for UpdateItem operations in DynamoDB and cannot be conditionally - * updated. UUIDs will be generated whenever the primary key attribute is missing or empty, regardless of any - * {@code UpdateBehavior} setting.

+ * or primary sort keys. Primary keys are immutable in DynamoDB and cannot use conditional update behaviors like + * {@link UpdateBehavior#WRITE_IF_NOT_EXISTS}. UUIDs will be generated whenever the primary key attribute is missing + * or empty, regardless of any {@code UpdateBehavior} setting.

* *

Secondary Index Keys: For GSI/LSI keys, {@link DynamoDbUpdateBehavior} can be used: *

    diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java index 7416fd92e5d2..30ceeaa952bf 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java @@ -16,6 +16,8 @@ import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Before; @@ -40,6 +42,8 @@ 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; /** * Tests for @DynamoDbAutoGeneratedKey annotation functionality. @@ -255,6 +259,119 @@ public void conflictingAnnotations_throwsException() { } + // ========== Transactional Write Operations Tests ========== + @Test + public void transactWrite_generatesKeysForMultipleOperations() { + TestRecord putRecord = new TestRecord(); + putRecord.setPayload("transact-put"); + TestRecord updateRecord = new TestRecord(); + updateRecord.setPayload("transact-update"); + + DynamoDbEnhancedClient client = createClient(); + client.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, putRecord) + .addUpdateItem(mappedTable, updateRecord) + .build()); + + List allRecords = mappedTable.scan().items().stream().collect(Collectors.toList()); + assertThat(allRecords).hasSize(2); + allRecords.forEach(this::assertAllKeysGenerated); + } + + @Test + public void transactWrite_preservesExistingKeysAndGeneratesMissing() { + TestRecord record = new TestRecord(); + record.setId("existing-id"); + record.setGsiPk("existing-gsi-pk"); + record.setPayload("transact-preserve"); + + DynamoDbEnhancedClient client = createClient(); + client.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + + TestRecord retrieved = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertThat(retrieved.getId()).isEqualTo("existing-id"); + assertThat(retrieved.getGsiPk()).isEqualTo("existing-gsi-pk"); + assertValidUuid(retrieved.getSortKey()); + assertValidUuid(retrieved.getGsiSk()); + } + + + // ========== Batch Write Operations Tests ========== + @Test + public void batchWrite_generatesAllMissingKeys() { + TestRecord record1 = new TestRecord(); + record1.setPayload("batch1"); + TestRecord record2 = new TestRecord(); + record2.setPayload("batch2"); + + DynamoDbEnhancedClient client = createClient(); + client.batchWriteItem(r -> r.addWriteBatch( + WriteBatch.builder(TestRecord.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build())); + + List allRecords = mappedTable.scan().items().stream().collect(Collectors.toList()); + assertThat(allRecords).hasSize(2); + allRecords.forEach(this::assertAllKeysGenerated); + } + + @Test + public void batchWrite_preservesExistingKeysAndGeneratesMissing() { + TestRecord record = new TestRecord(); + record.setId("existing-id"); + record.setSortKey("existing-sort"); + record.setPayload("batch-preserve"); + + DynamoDbEnhancedClient client = createClient(); + client.batchWriteItem(r -> r.addWriteBatch( + WriteBatch.builder(TestRecord.class) + .mappedTableResource(mappedTable) + .addPutItem(record) + .build())); + + TestRecord retrieved = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertThat(retrieved.getId()).isEqualTo("existing-id"); + assertThat(retrieved.getSortKey()).isEqualTo("existing-sort"); + assertValidUuid(retrieved.getGsiPk()); + assertValidUuid(retrieved.getGsiSk()); + } + + @Test + public void batchWrite_mixedPutDeleteOperations() { + TestRecord existingRecord = new TestRecord(); + existingRecord.setId("to-delete"); + existingRecord.setSortKey("to-delete-sort"); + mappedTable.putItem(existingRecord); + + TestRecord newRecord = new TestRecord(); + newRecord.setPayload("batch-mixed"); + + DynamoDbEnhancedClient client = createClient(); + client.batchWriteItem(r -> r.addWriteBatch( + WriteBatch.builder(TestRecord.class) + .mappedTableResource(mappedTable) + .addPutItem(newRecord) + .addDeleteItem(b -> b.key(k -> k.partitionValue("to-delete").sortValue("to-delete-sort"))) + .build())); + + List allRecords = mappedTable.scan().items().stream().collect(Collectors.toList()); + assertThat(allRecords).hasSize(1); + + TestRecord remaining = allRecords.get(0); + assertAllKeysGenerated(remaining); + assertThat(remaining.getPayload()).isEqualTo("batch-mixed"); + } + private DynamoDbEnhancedClient createClient() { return DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) @@ -266,6 +383,13 @@ private void deleteTableByName(String tableName) { getDynamoDbClient().deleteTable(b -> b.tableName(tableName)); } + private void assertAllKeysGenerated(TestRecord record) { + assertValidUuid(record.getId()); + assertValidUuid(record.getSortKey()); + assertValidUuid(record.getGsiPk()); + assertValidUuid(record.getGsiSk()); + } + @DynamoDbBean public static class TestRecord { private String id; From 031ef4476fa389bfcb5719b24a58381e975c5148 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Sun, 16 Nov 2025 20:34:32 +0200 Subject: [PATCH 03/14] Added support for DynamoDbAutoGeneratedKey annotation --- .../extensions/annotations/DynamoDbAutoGeneratedKey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java index 7a4c7ce90df5..a7021bb9f989 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java @@ -71,7 +71,7 @@ @SdkPublicApi @Documented @Retention(RetentionPolicy.RUNTIME) -@Target( {ElementType.METHOD, ElementType.FIELD}) +@Target({ElementType.METHOD, ElementType.FIELD}) @BeanTableSchemaAttributeTag(AutoGeneratedKeyTag.class) public @interface DynamoDbAutoGeneratedKey { } \ No newline at end of file From 8c4826dcfc99a670d0795dff649939f30694fac4 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Thu, 15 Jan 2026 10:57:10 +0200 Subject: [PATCH 04/14] Increased unit and integration test coverage to 100% --- .../AutoGeneratedKeyIntegrationTest.java | 359 ++++++++++++++++++ .../AutoGeneratedUuidIntegrationTest.java | 245 ++++++++++++ .../enhanced/dynamodb/model/BeanRecord.java | 79 ++++ .../AutogeneratedKeyConflictingRecord.java | 37 ++ .../AutogeneratedKeyInvalidTypeRecord.java | 35 ++ ...togeneratedKeyOnNonKeyAttributeRecord.java | 44 +++ .../AutogeneratedKeyRecord.java | 75 ++++ .../AutogeneratedKeyVersionedRecord.java | 55 +++ .../AutogeneratedUuidInvalidTypeRecord.java | 35 ++ .../AutogeneratedUuidRecord.java | 47 +++ .../extensions/AutoGeneratedKeyExtension.java | 2 +- .../AutoGeneratedKeyExtensionTest.java | 26 ++ .../AutoGeneratedUuidExtensionTest.java | 26 ++ .../VersionedRecordExtensionTest.java | 106 +++++- 14 files changed, 1169 insertions(+), 2 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedKeyIntegrationTest.java create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedUuidIntegrationTest.java create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/BeanRecord.java create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyConflictingRecord.java create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyInvalidTypeRecord.java create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyOnNonKeyAttributeRecord.java create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyRecord.java create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyVersionedRecord.java create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidInvalidTypeRecord.java create mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidRecord.java diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedKeyIntegrationTest.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedKeyIntegrationTest.java new file mode 100644 index 000000000000..56834bc840c5 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedKeyIntegrationTest.java @@ -0,0 +1,359 @@ +/* + * 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 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 org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.model.BeanRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; +import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyConflictingRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyInvalidTypeRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyOnNonKeyAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyVersionedRecord; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public class AutoGeneratedKeyIntegrationTest extends DynamoDbEnhancedIntegrationTestBase { + + private static final String TABLE_NAME = createTestTableName(); + + private static DynamoDbClient dynamoDbClient; + private static DynamoDbEnhancedClient enhancedClient; + private static DynamoDbTable mappedTable; + + @BeforeClass + public static void beforeClass() { + dynamoDbClient = createDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(dynamoDbClient) + .extensions(AutoGeneratedKeyExtension.builder().build()) + .build(); + mappedTable = enhancedClient.table(TABLE_NAME, TableSchema.fromClass(AutogeneratedKeyRecord.class)); + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)); + } + + @After + public void tearDown() { + mappedTable.scan() + .items() + .forEach(record -> mappedTable.deleteItem(record)); + } + + @AfterClass + public static void afterClass() { + try { + dynamoDbClient.deleteTable(r -> r.tableName(TABLE_NAME)); + } finally { + dynamoDbClient.close(); + } + } + + @Test + public void putItem_whenKeysNotPopulated_generatesNewUuids() { + AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); + + mappedTable.putItem(record); + AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + isValidUuid(result.getGsiPk()); + isValidUuid(result.getGsiSk()); + } + + @Test + public void putItem_whenKeysAlreadyPopulated_preservesExistingUuids() { + AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + record.setGsiPk("existing-gsiPk"); + record.setGsiSk("existing-gsiSk"); + + mappedTable.putItem(record); + AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertThat(result.getId()).isEqualTo("existing-id"); + assertThat(result.getSortKey()).isEqualTo("existing-sk"); + assertThat(result.getGsiPk()).isEqualTo("existing-gsiPk"); + assertThat(result.getGsiSk()).isEqualTo("existing-gsiSk"); + } + + @Test + public void updateItem_respectsUpdateBehavior() { + // put initial item + AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); + mappedTable.putItem(record); + AutogeneratedKeyRecord afterPut = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + String originalPk = afterPut.getId(); + String originalSk = afterPut.getSortKey(); + String originalGsiPk = afterPut.getGsiPk(); + String originalGsiSk = afterPut.getGsiSk(); + + + // update item + AutogeneratedKeyRecord update = new AutogeneratedKeyRecord(); + update.setId(afterPut.getId()); + update.setSortKey(afterPut.getSortKey()); + + mappedTable.updateItem(update); + AutogeneratedKeyRecord afterUpdate = + mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSortKey()))); + + // id and sortKey preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys + assertThat(afterUpdate.getId()).isEqualTo(originalPk); + assertThat(afterUpdate.getSortKey()).isEqualTo(originalSk); + + // gsiPk has WRITE_ALWAYS: regenerates UUID on every update + isValidUuid(afterUpdate.getGsiPk()); + assertThat(afterUpdate.getGsiPk()).isNotEqualTo(originalGsiPk); + + // gsiSk has WRITE_IF_NOT_EXISTS: preserves original UUID, only writes if null + assertThat(afterUpdate.getGsiSk()).isEqualTo(originalGsiSk); + } + + @Test + public void batchWrite_whenKeysNotPopulated_generatesNewUuids() { + AutogeneratedKeyRecord record1 = new AutogeneratedKeyRecord(); + AutogeneratedKeyRecord record2 = new AutogeneratedKeyRecord(); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(AutogeneratedKeyRecord.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build())); + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + + assertThat(results.size()).isEqualTo(2); + isValidUuid(results.get(0).getId()); + isValidUuid(results.get(1).getId()); + isValidUuid(results.get(0).getSortKey()); + isValidUuid(results.get(1).getSortKey()); + } + + @Test + public void batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { + AutogeneratedKeyRecord record1 = new AutogeneratedKeyRecord(); + record1.setId("existing-id-1"); + record1.setSortKey("existing-sk-1"); + record1.setGsiPk("existing-gsiPk-1"); + record1.setGsiSk("existing-gsiSk-1"); + + AutogeneratedKeyRecord record2 = new AutogeneratedKeyRecord(); + record2.setId("existing-id-2"); + record2.setSortKey("existing-sk-2"); + record2.setGsiPk("existing-gsiPk-2"); + record2.setGsiSk("existing-gsiSk-2"); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(AutogeneratedKeyRecord.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build())); + + AutogeneratedKeyRecord savedRecord1 = + mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id-1").sortValue("existing-sk-1"))); + assertThat(savedRecord1.getId()).isEqualTo("existing-id-1"); + assertThat(savedRecord1.getSortKey()).isEqualTo("existing-sk-1"); + assertThat(savedRecord1.getGsiPk()).isEqualTo("existing-gsiPk-1"); + assertThat(savedRecord1.getGsiSk()).isEqualTo("existing-gsiSk-1"); + + AutogeneratedKeyRecord savedRecord2 = + mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id-2").sortValue("existing-sk-2"))); + assertThat(savedRecord2.getId()).isEqualTo("existing-id-2"); + assertThat(savedRecord2.getSortKey()).isEqualTo("existing-sk-2"); + assertThat(savedRecord2.getGsiPk()).isEqualTo("existing-gsiPk-2"); + assertThat(savedRecord2.getGsiSk()).isEqualTo("existing-gsiSk-2"); + } + + @Test + public void transactWrite_whenKeysNotPopulated_generatesNewUuids() { + AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + + AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + isValidUuid(result.getGsiPk()); + isValidUuid(result.getGsiSk()); + } + + @Test + public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { + AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); + record.setId("existing-id-1"); + record.setSortKey("existing-sk-1"); + record.setGsiPk("existing-gsiPk-1"); + record.setGsiSk("existing-gsiSk-1"); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + + AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertThat(result.getId()).isEqualTo("existing-id-1"); + assertThat(result.getSortKey()).isEqualTo("existing-sk-1"); + assertThat(result.getGsiPk()).isEqualTo("existing-gsiPk-1"); + assertThat(result.getGsiSk()).isEqualTo("existing-gsiSk-1"); + } + + @Test + public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() { + String tableName = createTestTableName(); + + DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(dynamoDbClient) + .extensions(AutoGeneratedKeyExtension.builder().build(), + VersionedRecordExtension.builder().build()) + .build(); + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyVersionedRecord.class)); + + try { + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + + AutogeneratedKeyVersionedRecord record = new AutogeneratedKeyVersionedRecord(); + record.setData("data-v1"); + mappedTable.putItem(record); + + AutogeneratedKeyVersionedRecord retrieved = + mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + isValidUuid(retrieved.getId()); + assertThat(retrieved.getData()).isEqualTo("data-v1"); + assertThat(retrieved.getVersion()).isEqualTo(1L); + + retrieved.setData("data-v2"); + AutogeneratedKeyVersionedRecord updated = mappedTable.updateItem(retrieved); + isValidUuid(updated.getId()); + assertThat(updated.getData()).isEqualTo("data-v2"); + assertThat(updated.getVersion()).isEqualTo(2L); + + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void putItem_whenAnnotationInConflictWithAutogeneratedUuidAnnotation_throwsException() { + String tableName = createTestTableName(); + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyConflictingRecord.class)); + + try { + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + + assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyConflictingRecord())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid"); + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() { + String tableName = createTestTableName(); + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyOnNonKeyAttributeRecord.class)); + + try { + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + + assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyOnNonKeyAttributeRecord())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes"); + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void putItem_whenNoAutogeneratedKeyAnnotationIsPresent_doesNotRegenerateUuids() { + String tableName = createTestTableName(); + DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(BeanRecord.class)); + + try { + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + + BeanRecord record = new BeanRecord(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + record.setGsiPk("existing-gsiPk"); + record.setGsiSk("existing-gsiSk"); + record.setData("test"); + + mappedTable.putItem(record); + BeanRecord retrieved = mappedTable.getItem( + r -> r.key(k -> k.partitionValue("existing-id").sortValue("existing-sk"))); + 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("test"); + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void createBean_givenAutogeneratedKeyAnnotationAppliedOnNonStringAttributeType_throwsException() { + assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedKeyInvalidTypeRecord.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Key attribute") + .hasMessageContaining("Only String Class type is supported"); + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedUuidIntegrationTest.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedUuidIntegrationTest.java new file mode 100644 index 000000000000..2636da9d3b74 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedUuidIntegrationTest.java @@ -0,0 +1,245 @@ +/* + * 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 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 org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; +import software.amazon.awssdk.enhanced.dynamodb.model.BeanRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; +import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyConflictingRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedUuidInvalidTypeRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedUuidRecord; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public class AutoGeneratedUuidIntegrationTest extends DynamoDbEnhancedIntegrationTestBase { + + private static final String TABLE_NAME = createTestTableName(); + + private static DynamoDbClient dynamoDbClient; + private static DynamoDbEnhancedClient enhancedClient; + private static DynamoDbTable mappedTable; + + @BeforeClass + public static void beforeClass() { + dynamoDbClient = createDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(dynamoDbClient) + .extensions(AutoGeneratedUuidExtension.create()) + .build(); + mappedTable = enhancedClient.table(TABLE_NAME, TableSchema.fromClass(AutogeneratedUuidRecord.class)); + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)); + } + + @After + public void tearDown() { + mappedTable.scan() + .items() + .forEach(record -> mappedTable.deleteItem(record)); + } + + @AfterClass + public static void afterClass() { + try { + dynamoDbClient.deleteTable(r -> r.tableName(TABLE_NAME)); + } finally { + dynamoDbClient.close(); + } + } + + @Test + public void putItem_whenKeysNotPopulated_generatesNewUuids() { + AutogeneratedUuidRecord record = new AutogeneratedUuidRecord(); + + mappedTable.putItem(record); + AutogeneratedUuidRecord result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + } + + @Test + public void putItem_whenKeysAlreadyPopulated_replacesExistingUuids() { + AutogeneratedUuidRecord record = new AutogeneratedUuidRecord(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + + mappedTable.putItem(record); + AutogeneratedUuidRecord result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + assertThat(result.getId()).isNotEqualTo("existing-id"); + assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); + } + + @Test + public void batchWrite_whenKeysNotPopulated_generatesNewUuids() { + AutogeneratedUuidRecord record1 = new AutogeneratedUuidRecord(); + AutogeneratedUuidRecord record2 = new AutogeneratedUuidRecord(); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(AutogeneratedUuidRecord.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build())); + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + + assertThat(results.size()).isEqualTo(2); + isValidUuid(results.get(0).getId()); + isValidUuid(results.get(1).getId()); + isValidUuid(results.get(0).getSortKey()); + isValidUuid(results.get(1).getSortKey()); + } + + @Test + public void batchWrite_whenKeysAlreadyPopulated_generatesNewUuids() { + AutogeneratedUuidRecord record1 = new AutogeneratedUuidRecord(); + record1.setId("existing-id-1"); + record1.setSortKey("existing-sk-1"); + + AutogeneratedUuidRecord record2 = new AutogeneratedUuidRecord(); + record2.setId("existing-id-2"); + record2.setSortKey("existing-sk-2"); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(AutogeneratedUuidRecord.class) + .mappedTableResource(mappedTable) + .addPutItem(record1) + .addPutItem(record2) + .build())); + + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + + assertThat(results.size()).isEqualTo(2); + isValidUuid(results.get(0).getId()); + isValidUuid(results.get(1).getId()); + isValidUuid(results.get(0).getSortKey()); + isValidUuid(results.get(1).getSortKey()); + + 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_whenKeysNotPopulated_generatesNewUuids() { + AutogeneratedUuidRecord record = new AutogeneratedUuidRecord(); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + AutogeneratedUuidRecord result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + } + + @Test + public void transactWrite_whenKeysAlreadyPopulated_generatesNewUuids() { + AutogeneratedUuidRecord record = new AutogeneratedUuidRecord(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + + enhancedClient.transactWriteItems( + TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + AutogeneratedUuidRecord result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + assertThat(result.getId()).isNotEqualTo("existing-id"); + assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); + } + + @Test + public void putItem_whenAnnotationInConflictWithAutogeneratedKeyAnnotation_throwsException() { + String tableName = createTestTableName(); + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyConflictingRecord.class)); + + try { + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + + assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyConflictingRecord())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid"); + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_doesNotRegenerateUuids() { + String tableName = createTestTableName(); + DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(BeanRecord.class)); + + try { + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + + BeanRecord record = new BeanRecord(); + record.setId("existing-id"); + record.setSortKey("existing-sk"); + record.setGsiPk("existing-gsiPk"); + record.setGsiSk("existing-gsiSk"); + record.setData("data"); + + mappedTable.putItem(record); + BeanRecord 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_givenAutogeneratedUuidAnnotationAppliedOnNonStringAttributeType_throwsException() { + assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedUuidInvalidTypeRecord.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Uuid attribute") + .hasMessageContaining("Only String Class type is supported"); + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/BeanRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/BeanRecord.java new file mode 100644 index 000000000000..d493e9f19c06 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/BeanRecord.java @@ -0,0 +1,79 @@ +/* + * 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.model; + +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; + +@DynamoDbBean +public class BeanRecord { + 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; + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyConflictingRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyConflictingRecord.java new file mode 100644 index 000000000000..2ccf52378c9c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyConflictingRecord.java @@ -0,0 +1,37 @@ +/* + * 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.model.autogeneratedkeys; + +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class AutogeneratedKeyConflictingRecord { + private String id; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + @DynamoDbAutoGeneratedUuid + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyInvalidTypeRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyInvalidTypeRecord.java new file mode 100644 index 000000000000..6a6579230590 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyInvalidTypeRecord.java @@ -0,0 +1,35 @@ +/* + * 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.model.autogeneratedkeys; + +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class AutogeneratedKeyInvalidTypeRecord { + private Integer id; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyOnNonKeyAttributeRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyOnNonKeyAttributeRecord.java new file mode 100644 index 000000000000..1dfe7c1db5d1 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyOnNonKeyAttributeRecord.java @@ -0,0 +1,44 @@ +/* + * 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.model.autogeneratedkeys; + +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class AutogeneratedKeyOnNonKeyAttributeRecord { + private String id; + private String nonKeyAttribute; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbAutoGeneratedKey + public String getNonKeyAttribute() { + return nonKeyAttribute; + } + + public void setNonKeyAttribute(String nonKeyAttribute) { + this.nonKeyAttribute = nonKeyAttribute; + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyRecord.java new file mode 100644 index 000000000000..e04c660127d3 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyRecord.java @@ -0,0 +1,75 @@ +/* + * 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.model.autogeneratedkeys; + +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; +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; + +@DynamoDbBean +public class AutogeneratedKeyRecord { + private String id; + private String sortKey; + private String gsiPk; + private String gsiSk; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + @DynamoDbAutoGeneratedKey + public String getSortKey() { + return sortKey; + } + + public void setSortKey(String sortKey) { + this.sortKey = sortKey; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + public String getGsiPk() { + return gsiPk; + } + + public void setGsiPk(String gsiPk) { + this.gsiPk = gsiPk; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1") + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getGsiSk() { + return gsiSk; + } + + public void setGsiSk(String gsiSk) { + this.gsiSk = gsiSk; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyVersionedRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyVersionedRecord.java new file mode 100644 index 000000000000..f4f1d8a9c8ad --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyVersionedRecord.java @@ -0,0 +1,55 @@ +/* + * 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.model.autogeneratedkeys; + +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class AutogeneratedKeyVersionedRecord { + private String id; + private Long version; + private String data; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidInvalidTypeRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidInvalidTypeRecord.java new file mode 100644 index 000000000000..e437ab01ecf2 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidInvalidTypeRecord.java @@ -0,0 +1,35 @@ +/* + * 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.model.autogeneratedkeys; + +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class AutogeneratedUuidInvalidTypeRecord { + private Integer id; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedUuid + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidRecord.java new file mode 100644 index 000000000000..b7b0d2d1822a --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidRecord.java @@ -0,0 +1,47 @@ +/* + * 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.model.autogeneratedkeys; + +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; +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.DynamoDbSortKey; + +@DynamoDbBean +public class AutogeneratedUuidRecord { + 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; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java index 0898bb7e57d1..6d32376ef072 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java @@ -102,7 +102,7 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class) .orElse(null); - if (taggedAttributes == null || taggedAttributes.isEmpty()) { + if (taggedAttributes == null) { return WriteModification.builder().build(); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java index ee053f1a16fe..9d5f9277d294 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java @@ -370,6 +370,32 @@ public void conflictDetection_worksRegardlessOfExtensionOrder() { + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } + @Test + public void beforeWrite_noTaggedAttributes_returnsEmptyModification() { + StaticTableSchema schemaWithoutTags = + StaticTableSchema.builder(ItemWithKey.class) + .newItemSupplier(ItemWithKey::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithKey::getId) + .setter(ItemWithKey::setId) + .addTag(primaryPartitionKey())) + .build(); + + ItemWithKey item = new ItemWithKey(); + item.setId(RECORD_ID); + Map items = schemaWithoutTags.itemToMap(item, true); + + WriteModification result = extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(schemaWithoutTags.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + assertThat(result).isEqualTo(WriteModification.builder().build()); + } + private static class ItemWithKey { private String id; 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 cd92fa5fb063..00b5b632558e 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 @@ -195,6 +195,32 @@ public void conflictingAnnotations_throwsIllegalArgumentException() { + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } + @Test + public void beforeWrite_noCustomMetadata_returnsEmptyModification() { + StaticTableSchema schemaWithoutMetadata = + StaticTableSchema.builder(ItemWithUuid.class) + .newItemSupplier(ItemWithUuid::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithUuid::getId) + .setter(ItemWithUuid::setId) + .addTag(primaryPartitionKey())) + .build(); + + ItemWithUuid item = new ItemWithUuid(); + item.setId(RECORD_ID); + Map items = schemaWithoutMetadata.itemToMap(item, true); + + WriteModification result = atomicCounterExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(schemaWithoutMetadata.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); + + assertThat(result).isEqualTo(WriteModification.builder().build()); + } + 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/extensions/VersionedRecordExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java index 3f30fdc8ecdf..81622bd9b495 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java @@ -19,8 +19,11 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem.createUniqueFakeItem; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort.createUniqueFakeItemWithSort; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import java.util.HashMap; import java.util.Map; @@ -41,6 +44,7 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeVersionedStaticImmutableItem; import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext; +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.DynamoDbPartitionKey; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -273,7 +277,22 @@ public void customStartingValueAndIncrement_shouldThrow(Long startAt, Long incre public static Stream customFailingStartAtAndIncrementValues() { return Stream.of( Arguments.of(-2L, 1L), - Arguments.of(3L, 0L)); + Arguments.of(3L, 0L), + Arguments.of(-1L, 0L)); + } + + @Test + public void builder_startAtValueIsLessThanMinusOne_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, + () -> VersionedRecordExtension.builder().startAt(-2L).build(), + "startAt must be -1 or greater"); + } + + @Test + public void builder_incrementByValueIsLessThanOne_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, + () -> VersionedRecordExtension.builder().incrementBy(0L).build(), + "incrementBy must be greater than 0."); } @Test @@ -681,4 +700,89 @@ public static Stream customIncrementForExistingVersionValues() { Arguments.of(3L, null, 10L, "11"), Arguments.of(null, 3L, 4L, "7")); } + + @Test + public void versionAttribute_withInvalidStartAt_throwsIllegalArgumentException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + StaticTableSchema.builder(TestItem.class) + .newItemSupplier(TestItem::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(TestItem::getId) + .setter(TestItem::setId) + .addTag(primaryPartitionKey())) + .addAttribute(Long.class, + a -> a.name("version") + .getter(TestItem::getVersion) + .setter(TestItem::setVersion) + .addTag(versionAttribute(-2L, 1L))) + .build() + ) + .withMessage("startAt must be -1 or greater."); + } + + @Test + public void versionAttribute_withInvalidIncrementBy_throwsIllegalArgumentException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + StaticTableSchema.builder(TestItem.class) + .newItemSupplier(TestItem::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(TestItem::getId) + .setter(TestItem::setId) + .addTag(primaryPartitionKey())) + .addAttribute(Long.class, + a -> a.name("version") + .getter(TestItem::getVersion) + .setter(TestItem::setVersion) + .addTag(versionAttribute(0L, 0L))) + .build() + ) + .withMessage("incrementBy must be greater than 0."); + } + + @Test + public void versionAttribute_withNonNumericType_throwsIllegalArgumentException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> + StaticTableSchema.builder(TestItem.class) + .newItemSupplier(TestItem::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(TestItem::getId) + .setter(TestItem::setId) + .addTag(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("version") + .getter(TestItem::getId) + .setter(TestItem::setId) + .addTag(versionAttribute())) + .build() + ) + .withMessageContaining( + "is not a suitable type to be used as a version attribute. Only type 'N' is supported."); + } + + private static class TestItem { + private String id; + private Long version; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + } } From 79ba90f6119246ce644ea138fbb08ccf634cef11 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Thu, 12 Feb 2026 09:38:25 +0200 Subject: [PATCH 05/14] Increased code coverage --- ...-AmazonDynamoDBEnhancedClient-cbcc2bb.json | 2 +- .../AutoGeneratedUuidIntegrationTest.java | 245 -------------- .../enhanced/dynamodb/model/BeanRecord.java | 79 ----- .../AutogeneratedKeyConflictingRecord.java | 37 -- .../AutogeneratedKeyInvalidTypeRecord.java | 35 -- ...togeneratedKeyOnNonKeyAttributeRecord.java | 44 --- .../AutogeneratedKeyRecord.java | 75 ---- .../AutogeneratedKeyVersionedRecord.java | 55 --- .../AutogeneratedUuidInvalidTypeRecord.java | 35 -- .../AutogeneratedUuidRecord.java | 47 --- .../AutoGeneratedUuidExtension.java | 10 +- .../annotations/DynamoDbAutoGeneratedKey.java | 3 +- .../AutoGeneratedKeyExtensionTest.java | 23 +- .../AutoGeneratedUuidExtensionTest.java | 67 ++-- .../AutoGeneratedKeyRecordTest.java | 4 +- .../AutoGeneratedKeyExtensionTest.java} | 312 +++++++++++++---- .../AutoGeneratedUuidExtensionTest.java | 320 ++++++++++++++++++ 17 files changed, 616 insertions(+), 777 deletions(-) delete mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedUuidIntegrationTest.java delete mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/BeanRecord.java delete mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyConflictingRecord.java delete mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyInvalidTypeRecord.java delete mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyOnNonKeyAttributeRecord.java delete mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyRecord.java delete mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyVersionedRecord.java delete mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidInvalidTypeRecord.java delete mode 100644 services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidRecord.java rename services-custom/dynamodb-enhanced/src/{it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedKeyIntegrationTest.java => test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java} (61%) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json index b8c7700af796..81c91da75550 100644 --- a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json +++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json @@ -2,5 +2,5 @@ "type": "feature", "category": "Amazon DynamoDB Enhanced Client", "contributor": "", - "description": "Added the support for DynamoDbAutoGeneratedKey annotation" + "description": "Added support for DynamoDbAutoGeneratedKey annotation" } diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedUuidIntegrationTest.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedUuidIntegrationTest.java deleted file mode 100644 index 2636da9d3b74..000000000000 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedUuidIntegrationTest.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * 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 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 org.junit.After; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension; -import software.amazon.awssdk.enhanced.dynamodb.model.BeanRecord; -import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; -import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; -import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyConflictingRecord; -import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedUuidInvalidTypeRecord; -import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedUuidRecord; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; - -public class AutoGeneratedUuidIntegrationTest extends DynamoDbEnhancedIntegrationTestBase { - - private static final String TABLE_NAME = createTestTableName(); - - private static DynamoDbClient dynamoDbClient; - private static DynamoDbEnhancedClient enhancedClient; - private static DynamoDbTable mappedTable; - - @BeforeClass - public static void beforeClass() { - dynamoDbClient = createDynamoDbClient(); - enhancedClient = DynamoDbEnhancedClient.builder() - .dynamoDbClient(dynamoDbClient) - .extensions(AutoGeneratedUuidExtension.create()) - .build(); - mappedTable = enhancedClient.table(TABLE_NAME, TableSchema.fromClass(AutogeneratedUuidRecord.class)); - mappedTable.createTable(); - dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)); - } - - @After - public void tearDown() { - mappedTable.scan() - .items() - .forEach(record -> mappedTable.deleteItem(record)); - } - - @AfterClass - public static void afterClass() { - try { - dynamoDbClient.deleteTable(r -> r.tableName(TABLE_NAME)); - } finally { - dynamoDbClient.close(); - } - } - - @Test - public void putItem_whenKeysNotPopulated_generatesNewUuids() { - AutogeneratedUuidRecord record = new AutogeneratedUuidRecord(); - - mappedTable.putItem(record); - AutogeneratedUuidRecord result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); - } - - @Test - public void putItem_whenKeysAlreadyPopulated_replacesExistingUuids() { - AutogeneratedUuidRecord record = new AutogeneratedUuidRecord(); - record.setId("existing-id"); - record.setSortKey("existing-sk"); - - mappedTable.putItem(record); - AutogeneratedUuidRecord result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); - assertThat(result.getId()).isNotEqualTo("existing-id"); - assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); - } - - @Test - public void batchWrite_whenKeysNotPopulated_generatesNewUuids() { - AutogeneratedUuidRecord record1 = new AutogeneratedUuidRecord(); - AutogeneratedUuidRecord record2 = new AutogeneratedUuidRecord(); - - enhancedClient.batchWriteItem(req -> req.addWriteBatch( - WriteBatch.builder(AutogeneratedUuidRecord.class) - .mappedTableResource(mappedTable) - .addPutItem(record1) - .addPutItem(record2) - .build())); - List results = mappedTable.scan().items().stream().collect(Collectors.toList()); - - assertThat(results.size()).isEqualTo(2); - isValidUuid(results.get(0).getId()); - isValidUuid(results.get(1).getId()); - isValidUuid(results.get(0).getSortKey()); - isValidUuid(results.get(1).getSortKey()); - } - - @Test - public void batchWrite_whenKeysAlreadyPopulated_generatesNewUuids() { - AutogeneratedUuidRecord record1 = new AutogeneratedUuidRecord(); - record1.setId("existing-id-1"); - record1.setSortKey("existing-sk-1"); - - AutogeneratedUuidRecord record2 = new AutogeneratedUuidRecord(); - record2.setId("existing-id-2"); - record2.setSortKey("existing-sk-2"); - - enhancedClient.batchWriteItem(req -> req.addWriteBatch( - WriteBatch.builder(AutogeneratedUuidRecord.class) - .mappedTableResource(mappedTable) - .addPutItem(record1) - .addPutItem(record2) - .build())); - - List results = mappedTable.scan().items().stream().collect(Collectors.toList()); - - assertThat(results.size()).isEqualTo(2); - isValidUuid(results.get(0).getId()); - isValidUuid(results.get(1).getId()); - isValidUuid(results.get(0).getSortKey()); - isValidUuid(results.get(1).getSortKey()); - - 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_whenKeysNotPopulated_generatesNewUuids() { - AutogeneratedUuidRecord record = new AutogeneratedUuidRecord(); - - enhancedClient.transactWriteItems( - TransactWriteItemsEnhancedRequest.builder() - .addPutItem(mappedTable, record) - .build()); - AutogeneratedUuidRecord result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); - } - - @Test - public void transactWrite_whenKeysAlreadyPopulated_generatesNewUuids() { - AutogeneratedUuidRecord record = new AutogeneratedUuidRecord(); - record.setId("existing-id"); - record.setSortKey("existing-sk"); - - enhancedClient.transactWriteItems( - TransactWriteItemsEnhancedRequest.builder() - .addPutItem(mappedTable, record) - .build()); - AutogeneratedUuidRecord result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); - assertThat(result.getId()).isNotEqualTo("existing-id"); - assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); - } - - @Test - public void putItem_whenAnnotationInConflictWithAutogeneratedKeyAnnotation_throwsException() { - String tableName = createTestTableName(); - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyConflictingRecord.class)); - - try { - mappedTable.createTable(); - dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); - - assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyConflictingRecord())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid"); - } finally { - try { - mappedTable.deleteTable(); - } catch (Exception ignored) { - } - } - } - - @Test - public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_doesNotRegenerateUuids() { - String tableName = createTestTableName(); - DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(BeanRecord.class)); - - try { - mappedTable.createTable(); - dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); - - BeanRecord record = new BeanRecord(); - record.setId("existing-id"); - record.setSortKey("existing-sk"); - record.setGsiPk("existing-gsiPk"); - record.setGsiSk("existing-gsiSk"); - record.setData("data"); - - mappedTable.putItem(record); - BeanRecord 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_givenAutogeneratedUuidAnnotationAppliedOnNonStringAttributeType_throwsException() { - assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedUuidInvalidTypeRecord.class)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Uuid attribute") - .hasMessageContaining("Only String Class type is supported"); - } -} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/BeanRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/BeanRecord.java deleted file mode 100644 index d493e9f19c06..000000000000 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/BeanRecord.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.model; - -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; - -@DynamoDbBean -public class BeanRecord { - 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; - } -} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyConflictingRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyConflictingRecord.java deleted file mode 100644 index 2ccf52378c9c..000000000000 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyConflictingRecord.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.model.autogeneratedkeys; - -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; - -@DynamoDbBean -public class AutogeneratedKeyConflictingRecord { - private String id; - - @DynamoDbPartitionKey - @DynamoDbAutoGeneratedKey - @DynamoDbAutoGeneratedUuid - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } -} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyInvalidTypeRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyInvalidTypeRecord.java deleted file mode 100644 index 6a6579230590..000000000000 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyInvalidTypeRecord.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.model.autogeneratedkeys; - -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; - -@DynamoDbBean -public class AutogeneratedKeyInvalidTypeRecord { - private Integer id; - - @DynamoDbPartitionKey - @DynamoDbAutoGeneratedKey - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } -} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyOnNonKeyAttributeRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyOnNonKeyAttributeRecord.java deleted file mode 100644 index 1dfe7c1db5d1..000000000000 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyOnNonKeyAttributeRecord.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.model.autogeneratedkeys; - -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; - -@DynamoDbBean -public class AutogeneratedKeyOnNonKeyAttributeRecord { - private String id; - private String nonKeyAttribute; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - @DynamoDbAutoGeneratedKey - public String getNonKeyAttribute() { - return nonKeyAttribute; - } - - public void setNonKeyAttribute(String nonKeyAttribute) { - this.nonKeyAttribute = nonKeyAttribute; - } -} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyRecord.java deleted file mode 100644 index e04c660127d3..000000000000 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyRecord.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.model.autogeneratedkeys; - -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; -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; - -@DynamoDbBean -public class AutogeneratedKeyRecord { - private String id; - private String sortKey; - private String gsiPk; - private String gsiSk; - - @DynamoDbPartitionKey - @DynamoDbAutoGeneratedKey - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - @DynamoDbSortKey - @DynamoDbAutoGeneratedKey - public String getSortKey() { - return sortKey; - } - - public void setSortKey(String sortKey) { - this.sortKey = sortKey; - } - - @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) - public String getGsiPk() { - return gsiPk; - } - - public void setGsiPk(String gsiPk) { - this.gsiPk = gsiPk; - } - - @DynamoDbSecondarySortKey(indexNames = "gsi1") - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getGsiSk() { - return gsiSk; - } - - public void setGsiSk(String gsiSk) { - this.gsiSk = gsiSk; - } -} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyVersionedRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyVersionedRecord.java deleted file mode 100644 index f4f1d8a9c8ad..000000000000 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedKeyVersionedRecord.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.model.autogeneratedkeys; - -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; - -@DynamoDbBean -public class AutogeneratedKeyVersionedRecord { - private String id; - private Long version; - private String data; - - @DynamoDbPartitionKey - @DynamoDbAutoGeneratedKey - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - @DynamoDbVersionAttribute - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } -} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidInvalidTypeRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidInvalidTypeRecord.java deleted file mode 100644 index e437ab01ecf2..000000000000 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidInvalidTypeRecord.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.model.autogeneratedkeys; - -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; - -@DynamoDbBean -public class AutogeneratedUuidInvalidTypeRecord { - private Integer id; - - @DynamoDbPartitionKey - @DynamoDbAutoGeneratedUuid - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } -} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidRecord.java deleted file mode 100644 index b7b0d2d1822a..000000000000 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/autogeneratedkeys/AutogeneratedUuidRecord.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.model.autogeneratedkeys; - -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; -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.DynamoDbSortKey; - -@DynamoDbBean -public class AutogeneratedUuidRecord { - 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; - } -} 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 5a4712a177d3..884e12c20f89 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 @@ -40,12 +40,12 @@ * {@link java.util.UUID#randomUUID()} method. *

    * Key Difference from @DynamoDbAutoGeneratedKey: This extension always generates new UUIDs on every write, - * regardless of existing values. In contrast, {@code @DynamoDbAutoGeneratedKey} only generates UUIDs when the - * attribute value is null or empty, preserving existing values. + * regardless of existing values. In contrast, {@code @DynamoDbAutoGeneratedKey} only generates UUIDs when the attribute value is + * null or empty, preserving existing values. *

    * Conflict Detection: This extension cannot be used together with {@code @DynamoDbAutoGeneratedKey} on the same - * attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown - * at runtime to prevent unpredictable behavior. + * attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown at runtime to + * prevent unpredictable behavior. *

    * 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 @@ -93,7 +93,7 @@ public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientE */ private static final String KEY_EXTENSION_METADATA_KEY = "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute"; - + private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute(); private AutoGeneratedUuidExtension() { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java index a7021bb9f989..6fbb90d5a0c8 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java @@ -63,7 +63,8 @@ *

  • {@link UpdateBehavior#WRITE_ALWAYS} (default) – Generate a new UUID whenever the attribute is missing during write.
  • *
  • {@link UpdateBehavior#WRITE_IF_NOT_EXISTS} – Generate a UUID only on the first write, preserving the value on * subsequent updates.
  • - *

+ * + *

* *

Type restriction

* This annotation is only valid on attributes of type {@link String}. diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java index 9d5f9277d294..c19b5f793e3b 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java @@ -28,7 +28,7 @@ public class AutoGeneratedKeyExtensionTest { - private static final String RECORD_ID = "id123"; + private static final String RECORD_ID = "1"; private static final String TABLE_NAME = "table-name"; private static final OperationContext PRIMARY_CONTEXT = @@ -45,13 +45,13 @@ public class AutoGeneratedKeyExtensionTest { .addAttribute(String.class, a -> a.name("id") .getter(ItemWithKey::getId) .setter(ItemWithKey::setId) - .addTag(primaryPartitionKey()) // PK + .addTag(primaryPartitionKey()) .addTag(autoGeneratedKeyAttribute())) .addAttribute(String.class, a -> a.name("keyAttribute") .getter(ItemWithKey::getKeyAttribute) .setter(ItemWithKey::setKeyAttribute) .tags( - secondaryPartitionKey("gsi_keys_only"), // GSI + secondaryPartitionKey("gsi_keys_only"), autoGeneratedKeyAttribute())) .addAttribute(String.class, a -> a.name("simpleString") .getter(ItemWithKey::getSimpleString) @@ -71,7 +71,7 @@ public class AutoGeneratedKeyExtensionTest { .addAttribute(String.class, a -> a.name("keyAttribute") .getter(ItemWithKey::getKeyAttribute) .setter(ItemWithKey::setKeyAttribute) - // No index tags here — autogen on non-key fails at beforeWrite() + // index tags not defined — autogen on non-key fails at beforeWrite() .addTag(autoGeneratedKeyAttribute())) .addAttribute(String.class, a -> a.name("simpleString") .getter(ItemWithKey::getSimpleString) @@ -95,7 +95,7 @@ public class AutoGeneratedKeyExtensionTest { .getter(ItemWithKey::getSimpleString) .setter(ItemWithKey::setSimpleString) .tags( - secondarySortKey("lsi1"), // LSI + secondarySortKey("lsi1"), autoGeneratedKeyAttribute())) .build(); @@ -121,7 +121,6 @@ public void updateItem_withExistingKey_preservesValueAndDoesNotGenerateNew() { assertThat(transformed).isNotNull().hasSize(2); assertThat(transformed).containsEntry("id", AttributeValue.fromS(RECORD_ID)); - // Ensures the attribute remains a valid UUID without altering the preset value assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue(); assertThat(result.updateExpression()).isNull(); } @@ -197,7 +196,7 @@ public void nonStringTypeAnnotatedWithAutoGeneratedKey_throwsIllegalArgumentExce @Test public void autoGeneratedKey_onSecondaryPartitionKey_generatesUuid() { ItemWithKey item = new ItemWithKey(); - item.setId(RECORD_ID); // keyAttribute (GSI PK) is missing → should be generated + item.setId(RECORD_ID); Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); @@ -211,13 +210,13 @@ public void autoGeneratedKey_onSecondaryPartitionKey_generatesUuid() { Map transformed = result.transformedItem(); assertThat(transformed).isNotNull(); - assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue(); // generated for GSI PK + assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue(); } @Test public void autoGeneratedKey_onSecondarySortKey_generatesUuid() { ItemWithKey item = new ItemWithKey(); - item.setId(RECORD_ID); // simpleString (GSI/LSI) is missing → should be generated + item.setId(RECORD_ID); Map items = LSI_SK_AUTOGEN_SCHEMA.itemToMap(item, true); @@ -231,13 +230,13 @@ public void autoGeneratedKey_onSecondarySortKey_generatesUuid() { Map transformed = result.transformedItem(); assertThat(transformed).isNotNull(); - assertThat(isValidUuid(transformed.get("simpleString").s())).isTrue(); // generated for index SK + assertThat(isValidUuid(transformed.get("simpleString").s())).isTrue(); } @Test public void autoGeneratedKey_onNonKey_throwsIllegalArgumentException() { ItemWithKey item = new ItemWithKey(); - item.setId(RECORD_ID); // keyAttribute is annotated but NOT a key in this schema → should fail at beforeWrite() + item.setId(RECORD_ID); Map items = INVALID_NONKEY_AUTOGEN_SCHEMA.itemToMap(item, true); @@ -336,8 +335,6 @@ public void conflictingAnnotations_onSecondaryKey_throwsIllegalArgumentException @Test public void conflictDetection_worksRegardlessOfExtensionOrder() { - // Verify that AutoGeneratedKeyExtension detects conflicts even when - // AutoGeneratedUuidExtension metadata is already present StaticTableSchema conflictingSchema = StaticTableSchema .builder(ItemWithKey.class) 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 00b5b632558e..7bdb7e4cf3fb 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 @@ -65,6 +65,34 @@ public class AutoGeneratedUuidExtensionTest { .setter(ItemWithUuid::setSimpleString)) .build(); + @Test + public void beforeWrite_withNullCustomMetadataObject_returnsNoWriteModifications() { + StaticTableSchema schemaWithoutUuidAttribute = + StaticTableSchema.builder(ItemWithUuid.class) + .newItemSupplier(ItemWithUuid::new) + .addAttribute(String.class, a -> a.name("id") + .getter(ItemWithUuid::getId) + .setter(ItemWithUuid::setId) + .addTag(primaryPartitionKey())) + .build(); + + ItemWithUuid item = new ItemWithUuid(); + item.setId(RECORD_ID); + Map items = schemaWithoutUuidAttribute.itemToMap(item, true); + + WriteModification result = atomicCounterExtension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(schemaWithoutUuidAttribute.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT).build()); + + assertThat(result).isNotNull(); + assertThat(result.transformedItem()).isNull(); + assertThat(result.updateExpression()).isNull(); + assertThat(result.additionalConditionalExpression()).isNull(); + } + @Test public void beforeWrite_updateItemOperation_hasUuidInItem_doesNotCreateUpdateExpressionAndFilters() { ItemWithUuid SimpleItem = new ItemWithUuid(); @@ -156,45 +184,6 @@ void IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() { + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported."); } - @Test - public void conflictingAnnotations_throwsIllegalArgumentException() { - // Create a schema with both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid on the same attribute - StaticTableSchema conflictingSchema = - StaticTableSchema.builder(ItemWithUuid.class) - .newItemSupplier(ItemWithUuid::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(ItemWithUuid::getId) - .setter(ItemWithUuid::setId) - .addTag(primaryPartitionKey()) - // Both annotations on the same attribute - .addTag(AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute()) - .addTag(AutoGeneratedKeyExtension.AttributeTags.autoGeneratedKeyAttribute())) - .addAttribute(String.class, - a -> a.name("simpleString") - .getter(ItemWithUuid::getSimpleString) - .setter(ItemWithUuid::setSimpleString)) - .build(); - - ItemWithUuid item = new ItemWithUuid(); - item.setId(RECORD_ID); - - Map items = conflictingSchema.itemToMap(item, true); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> - atomicCounterExtension.beforeWrite( - DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(conflictingSchema.tableMetadata()) - .operationName(OperationName.PUT_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build()) - ) - .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " - + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); - } - @Test public void beforeWrite_noCustomMetadata_returnsEmptyModification() { StaticTableSchema schemaWithoutMetadata = diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java index 30ceeaa952bf..506789cadd0c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java @@ -339,7 +339,7 @@ public void batchWrite_preservesExistingKeysAndGeneratesMissing() { TestRecord retrieved = mappedTable.scan().items().stream().findFirst() .orElseThrow(() -> new AssertionError("No record found")); - + assertThat(retrieved.getId()).isEqualTo("existing-id"); assertThat(retrieved.getSortKey()).isEqualTo("existing-sort"); assertValidUuid(retrieved.getGsiPk()); @@ -366,7 +366,7 @@ public void batchWrite_mixedPutDeleteOperations() { List allRecords = mappedTable.scan().items().stream().collect(Collectors.toList()); assertThat(allRecords).hasSize(1); - + TestRecord remaining = allRecords.get(0); assertAllKeysGenerated(remaining); assertThat(remaining.getPayload()).isEqualTo("batch-mixed"); diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedKeyIntegrationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java similarity index 61% rename from services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedKeyIntegrationTest.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java index 56834bc840c5..52f80818216e 100644 --- a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/AutoGeneratedKeyIntegrationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.enhanced.dynamodb; +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.extensions; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -21,56 +21,54 @@ import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.After; -import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.junit.Before; import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension; -import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension; -import software.amazon.awssdk.enhanced.dynamodb.model.BeanRecord; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedUuid; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; +import software.amazon.awssdk.enhanced.dynamodb.mapper.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; -import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyConflictingRecord; -import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyInvalidTypeRecord; -import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyOnNonKeyAttributeRecord; -import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyRecord; -import software.amazon.awssdk.enhanced.dynamodb.model.autogeneratedkeys.AutogeneratedKeyVersionedRecord; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; - -public class AutoGeneratedKeyIntegrationTest extends DynamoDbEnhancedIntegrationTestBase { - - private static final String TABLE_NAME = createTestTableName(); - - private static DynamoDbClient dynamoDbClient; - private static DynamoDbEnhancedClient enhancedClient; - private static DynamoDbTable mappedTable; - - @BeforeClass - public static void beforeClass() { - dynamoDbClient = createDynamoDbClient(); - enhancedClient = DynamoDbEnhancedClient.builder() - .dynamoDbClient(dynamoDbClient) - .extensions(AutoGeneratedKeyExtension.builder().build()) - .build(); - mappedTable = enhancedClient.table(TABLE_NAME, TableSchema.fromClass(AutogeneratedKeyRecord.class)); - mappedTable.createTable(); - dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)); - } - @After - public void tearDown() { - mappedTable.scan() - .items() - .forEach(record -> mappedTable.deleteItem(record)); +public class AutoGeneratedKeyExtensionTest extends LocalDynamoDbSyncTestBase { + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(AutogeneratedKeyRecord.class); + + private final DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(Stream.concat( + ExtensionResolver.defaultExtensions().stream(), + Stream.of(AutoGeneratedKeyExtension.builder().build())) + .collect(Collectors.toList())) + .build(); + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName("autogenerated-key-table"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); } - @AfterClass - public static void afterClass() { - try { - dynamoDbClient.deleteTable(r -> r.tableName(TABLE_NAME)); - } finally { - dynamoDbClient.close(); - } + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("autogenerated-key-table"))); } @Test @@ -236,27 +234,20 @@ public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { @Test public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() { - String tableName = createTestTableName(); - - DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() - .dynamoDbClient(dynamoDbClient) - .extensions(AutoGeneratedKeyExtension.builder().build(), - VersionedRecordExtension.builder().build()) - .build(); + String tableName = "versioned-record-autogenerated-key-table"; DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyVersionedRecord.class)); try { - mappedTable.createTable(); - dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); AutogeneratedKeyVersionedRecord record = new AutogeneratedKeyVersionedRecord(); + record.setId("id"); record.setData("data-v1"); mappedTable.putItem(record); - AutogeneratedKeyVersionedRecord retrieved = - mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); + AutogeneratedKeyVersionedRecord retrieved = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); isValidUuid(retrieved.getId()); assertThat(retrieved.getData()).isEqualTo("data-v1"); assertThat(retrieved.getVersion()).isEqualTo(1L); @@ -277,13 +268,12 @@ public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() { @Test public void putItem_whenAnnotationInConflictWithAutogeneratedUuidAnnotation_throwsException() { - String tableName = createTestTableName(); + String tableName = "conflicting-annotations-record-autogenerated-key-table"; DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyConflictingRecord.class)); try { - mappedTable.createTable(); - dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyConflictingRecord())) .isInstanceOf(IllegalArgumentException.class) @@ -298,13 +288,12 @@ public void putItem_whenAnnotationInConflictWithAutogeneratedUuidAnnotation_thro @Test public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() { - String tableName = createTestTableName(); + String tableName = "annotation-on-non-key-attribute-record-autogenerated-key-table"; DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyOnNonKeyAttributeRecord.class)); try { - mappedTable.createTable(); - dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyOnNonKeyAttributeRecord())) .isInstanceOf(IllegalArgumentException.class) @@ -319,12 +308,11 @@ public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() { @Test public void putItem_whenNoAutogeneratedKeyAnnotationIsPresent_doesNotRegenerateUuids() { - String tableName = createTestTableName(); + String tableName = "no-annotation-record-autogenerated-key-table"; DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(BeanRecord.class)); try { - mappedTable.createTable(); - dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(tableName)); + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); BeanRecord record = new BeanRecord(); record.setId("existing-id"); @@ -356,4 +344,200 @@ public void createBean_givenAutogeneratedKeyAnnotationAppliedOnNonStringAttribut .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Key attribute") .hasMessageContaining("Only String Class type is supported"); } + + @DynamoDbBean + public static class BeanRecord { + 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 AutogeneratedKeyRecord { + private String id; + private String sortKey; + private String gsiPk; + private String gsiSk; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + @DynamoDbAutoGeneratedKey + public String getSortKey() { + return sortKey; + } + + public void setSortKey(String sortKey) { + this.sortKey = sortKey; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + public String getGsiPk() { + return gsiPk; + } + + public void setGsiPk(String gsiPk) { + this.gsiPk = gsiPk; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1") + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getGsiSk() { + return gsiSk; + } + + public void setGsiSk(String gsiSk) { + this.gsiSk = gsiSk; + } + } + + @DynamoDbBean + public static class AutogeneratedKeyVersionedRecord { + private String id; + private Long version; + private String data; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + @DynamoDbBean + public static class AutogeneratedKeyInvalidTypeRecord { + private Integer id; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } + + @DynamoDbBean + public static class AutogeneratedKeyOnNonKeyAttributeRecord { + private String id; + private String nonKeyAttribute; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbAutoGeneratedKey + public String getNonKeyAttribute() { + return nonKeyAttribute; + } + + public void setNonKeyAttribute(String nonKeyAttribute) { + this.nonKeyAttribute = nonKeyAttribute; + } + } + + @DynamoDbBean + public static class AutogeneratedKeyConflictingRecord { + private String id; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + @DynamoDbAutoGeneratedUuid + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } } + + 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..03588232bd2b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java @@ -0,0 +1,320 @@ +/* + * 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_whenKeysNotPopulated_generatesNewUuids() { + RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); + record.setId("existing-id"); + + mappedTable.putItem(record); + RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + } + + @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")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + assertThat(result.getId()).isNotEqualTo("existing-id"); + assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); + } + + @Test + public void batchWrite_whenKeysNotPopulated_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); + isValidUuid(results.get(0).getId()); + isValidUuid(results.get(1).getId()); + isValidUuid(results.get(0).getSortKey()); + isValidUuid(results.get(1).getSortKey()); + } + + @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); + isValidUuid(results.get(0).getId()); + isValidUuid(results.get(1).getId()); + isValidUuid(results.get(0).getSortKey()); + isValidUuid(results.get(1).getSortKey()); + + 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_whenKeysNotPopulated_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")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + } + + @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")); + + isValidUuid(result.getId()); + isValidUuid(result.getSortKey()); + assertThat(result.getId()).isNotEqualTo("existing-id"); + assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); + } + + @Test + public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_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_givenAutogeneratedUuidAnnotationAppliedOnNonStringAttributeType_throwsException() { + assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedUuidInvalidTypeRecord.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Uuid attribute") + .hasMessageContaining("Only String Class type is supported"); + } + + @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 From 8ae8405339417007fe460f48262875cf1cbb57d9 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Thu, 12 Feb 2026 15:02:24 +0200 Subject: [PATCH 06/14] Tests refactoring --- .../extensions/AutoGeneratedKeyExtension.java | 15 +- .../AutoGeneratedKeyExtensionTest.java | 168 +++++++++--------- .../AutoGeneratedUuidExtensionTest.java | 4 +- .../ConflictingAnnotationsTest.java | 92 +++++----- .../VersionedRecordExtensionTest.java | 120 +------------ .../AutoGeneratedKeyRecordTest.java | 20 ++- .../AutoGeneratedUuidRecordTest.java | 8 +- .../AutoGeneratedKeyExtensionTest.java | 39 ++-- .../AutoGeneratedUuidExtensionTest.java | 12 +- 9 files changed, 184 insertions(+), 294 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java index 6d32376ef072..9db27b1ff6d8 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java @@ -121,18 +121,17 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex + "and cannot be used together on the same attribute."); }); - TableMetadata meta = context.tableMetadata(); + TableMetadata metadata = context.tableMetadata(); Set allowedKeys = new HashSet<>(); // ensure every @DynamoDbAutoGeneratedKey attribute is a PK/SK or GSI/LSI. If not, throw IllegalArgumentException - allowedKeys.add(meta.primaryPartitionKey()); - meta.primarySortKey().ifPresent(allowedKeys::add); + allowedKeys.add(metadata.primaryPartitionKey()); + metadata.primarySortKey().ifPresent(allowedKeys::add); - for (IndexMetadata idx : meta.indices()) { - String indexName = idx.name(); - allowedKeys.add(meta.indexPartitionKey(indexName)); - meta.indexSortKey(indexName).ifPresent(allowedKeys::add); - } + metadata.indices().stream().map(IndexMetadata::name).forEach(indexName -> { + allowedKeys.add(metadata.indexPartitionKey(indexName)); + metadata.indexSortKey(indexName).ifPresent(allowedKeys::add); + }); taggedAttributes.stream() .filter(attr -> !allowedKeys.contains(attr)) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java index c19b5f793e3b..5b57765872c6 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java @@ -37,71 +37,71 @@ public class AutoGeneratedKeyExtensionTest { private final AutoGeneratedKeyExtension extension = AutoGeneratedKeyExtension.builder().build(); /** - * Schema that places @DynamoDbAutoGeneratedKey on GSI key ("keyAttribute") so the validation passes. + * Schema that places @DynamoDbAutoGeneratedKey on GSI key ("keyAttribute") -> the validation passes. */ - private static final StaticTableSchema ITEM_WITH_KEY_SCHEMA = - StaticTableSchema.builder(ItemWithKey.class) - .newItemSupplier(ItemWithKey::new) + private static final StaticTableSchema ITEM_WITH_KEY_SCHEMA = + StaticTableSchema.builder(AutogeneratedKeyItem.class) + .newItemSupplier(AutogeneratedKeyItem::new) .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithKey::getId) - .setter(ItemWithKey::setId) + .getter(AutogeneratedKeyItem::getId) + .setter(AutogeneratedKeyItem::setId) .addTag(primaryPartitionKey()) .addTag(autoGeneratedKeyAttribute())) .addAttribute(String.class, a -> a.name("keyAttribute") - .getter(ItemWithKey::getKeyAttribute) - .setter(ItemWithKey::setKeyAttribute) + .getter(AutogeneratedKeyItem::getKeyAttribute) + .setter(AutogeneratedKeyItem::setKeyAttribute) .tags( secondaryPartitionKey("gsi_keys_only"), autoGeneratedKeyAttribute())) .addAttribute(String.class, a -> a.name("simpleString") - .getter(ItemWithKey::getSimpleString) - .setter(ItemWithKey::setSimpleString)) + .getter(AutogeneratedKeyItem::getSimpleString) + .setter(AutogeneratedKeyItem::setSimpleString)) .build(); /** - * Schema that places @DynamoDbAutoGeneratedKey on a NON-KEY attribute to trigger the exception. + * Schema that places @DynamoDbAutoGeneratedKey on a NON-KEY attribute -> triggers the exception. */ - private static final StaticTableSchema INVALID_NONKEY_AUTOGEN_SCHEMA = - StaticTableSchema.builder(ItemWithKey.class) - .newItemSupplier(ItemWithKey::new) + private static final StaticTableSchema INVALID_NONKEY_AUTOGEN_SCHEMA = + StaticTableSchema.builder(AutogeneratedKeyItem.class) + .newItemSupplier(AutogeneratedKeyItem::new) .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithKey::getId) - .setter(ItemWithKey::setId) + .getter(AutogeneratedKeyItem::getId) + .setter(AutogeneratedKeyItem::setId) .addTag(primaryPartitionKey())) .addAttribute(String.class, a -> a.name("keyAttribute") - .getter(ItemWithKey::getKeyAttribute) - .setter(ItemWithKey::setKeyAttribute) + .getter(AutogeneratedKeyItem::getKeyAttribute) + .setter(AutogeneratedKeyItem::setKeyAttribute) // index tags not defined — autogen on non-key fails at beforeWrite() .addTag(autoGeneratedKeyAttribute())) .addAttribute(String.class, a -> a.name("simpleString") - .getter(ItemWithKey::getSimpleString) - .setter(ItemWithKey::setSimpleString)) + .getter(AutogeneratedKeyItem::getSimpleString) + .setter(AutogeneratedKeyItem::setSimpleString)) .build(); /** - * Schema that places @DynamoDbAutoGeneratedKey on LSI key ("simpleString") so the validation passes. + * Schema that places @DynamoDbAutoGeneratedKey on LSI key ("simpleString") -> the validation passes. */ - private static final StaticTableSchema LSI_SK_AUTOGEN_SCHEMA = - StaticTableSchema.builder(ItemWithKey.class) - .newItemSupplier(ItemWithKey::new) + private static final StaticTableSchema LSI_SK_AUTOGEN_SCHEMA = + StaticTableSchema.builder(AutogeneratedKeyItem.class) + .newItemSupplier(AutogeneratedKeyItem::new) .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithKey::getId) - .setter(ItemWithKey::setId) + .getter(AutogeneratedKeyItem::getId) + .setter(AutogeneratedKeyItem::setId) .addTag(primaryPartitionKey())) .addAttribute(String.class, a -> a.name("keyAttribute") - .getter(ItemWithKey::getKeyAttribute) - .setter(ItemWithKey::setKeyAttribute)) + .getter(AutogeneratedKeyItem::getKeyAttribute) + .setter(AutogeneratedKeyItem::setKeyAttribute)) .addAttribute(String.class, a -> a.name("simpleString") - .getter(ItemWithKey::getSimpleString) - .setter(ItemWithKey::setSimpleString) + .getter(AutogeneratedKeyItem::getSimpleString) + .setter(AutogeneratedKeyItem::setSimpleString) .tags( secondarySortKey("lsi1"), autoGeneratedKeyAttribute())) .build(); @Test - public void updateItem_withExistingKey_preservesValueAndDoesNotGenerateNew() { - ItemWithKey item = new ItemWithKey(); + public void updateItem_withExistingKey_preservesValueAndDoesNotGenerateNewOne() { + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); String preset = UUID.randomUUID().toString(); item.setKeyAttribute(preset); @@ -126,8 +126,8 @@ public void updateItem_withExistingKey_preservesValueAndDoesNotGenerateNew() { } @Test - public void updateItem_withoutKey_generatesNewUuid() { - ItemWithKey item = new ItemWithKey(); + public void updateItem_withoutExistingKey_generatesNewUuid() { + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); @@ -150,7 +150,7 @@ public void updateItem_withoutKey_generatesNewUuid() { @Test public void updateItem_withMissingKeyAttribute_insertsGeneratedUuid() { - ItemWithKey item = new ItemWithKey(); + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); @@ -171,22 +171,22 @@ public void updateItem_withMissingKeyAttribute_insertsGeneratedUuid() { } @Test - public void nonStringTypeAnnotatedWithAutoGeneratedKey_throwsIllegalArgumentException() { + public void nonStringAttributeAnnotatedWithAutoGeneratedKey_throwsIllegalArgumentException() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> - StaticTableSchema.builder(ItemWithKey.class) - .newItemSupplier(ItemWithKey::new) + StaticTableSchema.builder(AutogeneratedKeyItem.class) + .newItemSupplier(AutogeneratedKeyItem::new) .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithKey::getId) - .setter(ItemWithKey::setId) + .getter(AutogeneratedKeyItem::getId) + .setter(AutogeneratedKeyItem::setId) .addTag(primaryPartitionKey())) .addAttribute(Integer.class, a -> a.name("intAttribute") - .getter(ItemWithKey::getIntAttribute) - .setter(ItemWithKey::setIntAttribute) + .getter(AutogeneratedKeyItem::getIntAttribute) + .setter(AutogeneratedKeyItem::setIntAttribute) .addTag(autoGeneratedKeyAttribute())) .addAttribute(String.class, a -> a.name("simpleString") - .getter(ItemWithKey::getSimpleString) - .setter(ItemWithKey::setSimpleString)) + .getter(AutogeneratedKeyItem::getSimpleString) + .setter(AutogeneratedKeyItem::setSimpleString)) .build() ) .withMessage("Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type " @@ -195,7 +195,7 @@ public void nonStringTypeAnnotatedWithAutoGeneratedKey_throwsIllegalArgumentExce @Test public void autoGeneratedKey_onSecondaryPartitionKey_generatesUuid() { - ItemWithKey item = new ItemWithKey(); + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); @@ -215,7 +215,7 @@ public void autoGeneratedKey_onSecondaryPartitionKey_generatesUuid() { @Test public void autoGeneratedKey_onSecondarySortKey_generatesUuid() { - ItemWithKey item = new ItemWithKey(); + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); Map items = LSI_SK_AUTOGEN_SCHEMA.itemToMap(item, true); @@ -234,8 +234,8 @@ public void autoGeneratedKey_onSecondarySortKey_generatesUuid() { } @Test - public void autoGeneratedKey_onNonKey_throwsIllegalArgumentException() { - ItemWithKey item = new ItemWithKey(); + public void autoGeneratedKey_onNonKeyAttribute_throwsIllegalArgumentException() { + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); Map items = INVALID_NONKEY_AUTOGEN_SCHEMA.itemToMap(item, true); @@ -255,25 +255,25 @@ public void autoGeneratedKey_onNonKey_throwsIllegalArgumentException() { } @Test - public void conflictingAnnotations_throwsIllegalArgumentException() { + public void conflictingAnnotations_onSameAttribute_throwsIllegalArgumentException() { // Create a schema with both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid on the same attribute - StaticTableSchema conflictingSchema = + StaticTableSchema conflictingSchema = StaticTableSchema - .builder(ItemWithKey.class) - .newItemSupplier(ItemWithKey::new) + .builder(AutogeneratedKeyItem.class) + .newItemSupplier(AutogeneratedKeyItem::new) .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithKey::getId) - .setter(ItemWithKey::setId) + .getter(AutogeneratedKeyItem::getId) + .setter(AutogeneratedKeyItem::setId) .addTag(primaryPartitionKey()) // Both annotations on the same attribute .addTag(autoGeneratedKeyAttribute()) .addTag(autoGeneratedUuidAttribute())) .addAttribute(String.class, a -> a.name("simpleString") - .getter(ItemWithKey::getSimpleString) - .setter(ItemWithKey::setSimpleString)) + .getter(AutogeneratedKeyItem::getSimpleString) + .setter(AutogeneratedKeyItem::setSimpleString)) .build(); - ItemWithKey item = new ItemWithKey(); + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); Map items = conflictingSchema.itemToMap(item, true); @@ -294,27 +294,27 @@ public void conflictingAnnotations_throwsIllegalArgumentException() { @Test public void conflictingAnnotations_onSecondaryKey_throwsIllegalArgumentException() { // Create a schema with both annotations on a GSI key - StaticTableSchema conflictingGsiSchema = + StaticTableSchema conflictingGsiSchema = StaticTableSchema - .builder(ItemWithKey.class) - .newItemSupplier(ItemWithKey::new) + .builder(AutogeneratedKeyItem.class) + .newItemSupplier(AutogeneratedKeyItem::new) .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithKey::getId) - .setter(ItemWithKey::setId) + .getter(AutogeneratedKeyItem::getId) + .setter(AutogeneratedKeyItem::setId) .addTag(primaryPartitionKey())) .addAttribute(String.class, a -> a.name("keyAttribute") - .getter(ItemWithKey::getKeyAttribute) - .setter(ItemWithKey::setKeyAttribute) + .getter(AutogeneratedKeyItem::getKeyAttribute) + .setter(AutogeneratedKeyItem::setKeyAttribute) .addTag(secondaryPartitionKey("gsi1")) // Both annotations on the same GSI key .addTag(autoGeneratedKeyAttribute()) .addTag(autoGeneratedUuidAttribute())) .addAttribute(String.class, a -> a.name("simpleString") - .getter(ItemWithKey::getSimpleString) - .setter(ItemWithKey::setSimpleString)) + .getter(AutogeneratedKeyItem::getSimpleString) + .setter(AutogeneratedKeyItem::setSimpleString)) .build(); - ItemWithKey item = new ItemWithKey(); + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); Map items = conflictingGsiSchema.itemToMap(item, true); @@ -335,19 +335,19 @@ public void conflictingAnnotations_onSecondaryKey_throwsIllegalArgumentException @Test public void conflictDetection_worksRegardlessOfExtensionOrder() { - StaticTableSchema conflictingSchema = + StaticTableSchema conflictingSchema = StaticTableSchema - .builder(ItemWithKey.class) - .newItemSupplier(ItemWithKey::new) + .builder(AutogeneratedKeyItem.class) + .newItemSupplier(AutogeneratedKeyItem::new) .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithKey::getId) - .setter(ItemWithKey::setId) + .getter(AutogeneratedKeyItem::getId) + .setter(AutogeneratedKeyItem::setId) .addTag(primaryPartitionKey()) .addTag(autoGeneratedKeyAttribute()) .addTag(autoGeneratedUuidAttribute())) .build(); - ItemWithKey item = new ItemWithKey(); + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); Map items = conflictingSchema.itemToMap(item, true); @@ -368,17 +368,17 @@ public void conflictDetection_worksRegardlessOfExtensionOrder() { } @Test - public void beforeWrite_noTaggedAttributes_returnsEmptyModification() { - StaticTableSchema schemaWithoutTags = - StaticTableSchema.builder(ItemWithKey.class) - .newItemSupplier(ItemWithKey::new) + public void beforeWrite_noAttributesTaggedWithAutogeneratedKey_returnsEmptyModification() { + StaticTableSchema schemaWithoutTags = + StaticTableSchema.builder(AutogeneratedKeyItem.class) + .newItemSupplier(AutogeneratedKeyItem::new) .addAttribute(String.class, a -> a.name("id") - .getter(ItemWithKey::getId) - .setter(ItemWithKey::setId) + .getter(AutogeneratedKeyItem::getId) + .setter(AutogeneratedKeyItem::setId) .addTag(primaryPartitionKey())) .build(); - ItemWithKey item = new ItemWithKey(); + AutogeneratedKeyItem item = new AutogeneratedKeyItem(); item.setId(RECORD_ID); Map items = schemaWithoutTags.itemToMap(item, true); @@ -393,14 +393,14 @@ public void beforeWrite_noTaggedAttributes_returnsEmptyModification() { assertThat(result).isEqualTo(WriteModification.builder().build()); } - private static class ItemWithKey { + private static class AutogeneratedKeyItem { private String id; private String keyAttribute; private String simpleString; private Integer intAttribute; - ItemWithKey() { + AutogeneratedKeyItem() { } public Integer getIntAttribute() { @@ -440,10 +440,10 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof ItemWithKey)) { + if (!(o instanceof AutogeneratedKeyItem)) { return false; } - ItemWithKey that = (ItemWithKey) o; + AutogeneratedKeyItem that = (AutogeneratedKeyItem) o; return Objects.equals(id, that.id) && Objects.equals(keyAttribute, that.keyAttribute) && Objects.equals(simpleString, that.simpleString); 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 7bdb7e4cf3fb..d506fced8d6f 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 @@ -66,7 +66,7 @@ public class AutoGeneratedUuidExtensionTest { .build(); @Test - public void beforeWrite_withNullCustomMetadataObject_returnsNoWriteModifications() { + public void beforeWrite_withNoMetadata_returnsNoWriteModifications() { StaticTableSchema schemaWithoutUuidAttribute = StaticTableSchema.builder(ItemWithUuid.class) .newItemSupplier(ItemWithUuid::new) @@ -185,7 +185,7 @@ void IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() { } @Test - public void beforeWrite_noCustomMetadata_returnsEmptyModification() { + public void beforeWrite_withoutMetadata_returnsEmptyModification() { StaticTableSchema schemaWithoutMetadata = StaticTableSchema.builder(ItemWithUuid.class) .newItemSupplier(ItemWithUuid::new) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java index c2ed794ee310..7b7d01c7afce 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java @@ -36,8 +36,8 @@ */ public class ConflictingAnnotationsTest { - private static final String RECORD_ID = "test-id"; - private static final String TABLE_NAME = "test-table"; + private static final String RECORD_ID = "1"; + private static final String TABLE_NAME = "table-name"; private static final OperationContext PRIMARY_CONTEXT = DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName()); @@ -47,12 +47,12 @@ public class ConflictingAnnotationsTest { /** * Schema with both annotations on the same attribute to test conflict detection. */ - private static final StaticTableSchema CONFLICTING_SCHEMA = - StaticTableSchema.builder(TestItem.class) - .newItemSupplier(TestItem::new) - .addAttribute(String.class, a -> a.name("id") - .getter(TestItem::getId) - .setter(TestItem::setId) + private static final StaticTableSchema CONFLICTING_SCHEMA = + StaticTableSchema.builder(RecordWithAutogenerated.class) + .newItemSupplier(RecordWithAutogenerated::new) + .addAttribute(String.class, a -> a.name("autogeneratedKeyField") + .getter(RecordWithAutogenerated::getAutogeneratedKeyField) + .setter(RecordWithAutogenerated::setAutogeneratedKeyField) .addTag(primaryPartitionKey()) // Both annotations on the same attribute .addTag(autoGeneratedKeyAttribute()) @@ -60,9 +60,9 @@ public class ConflictingAnnotationsTest { .build(); @Test - public void keyExtensionFirst_detectsConflictWithUuidExtension() { - TestItem item = new TestItem(); - item.setId(RECORD_ID); + public void autogeneratedKeyExtensionFirst_detectsConflictWithUuidExtension() { + RecordWithAutogenerated item = new RecordWithAutogenerated(); + item.setAutogeneratedKeyField(RECORD_ID); Map items = CONFLICTING_SCHEMA.itemToMap(item, true); @@ -76,14 +76,15 @@ public void keyExtensionFirst_detectsConflictWithUuidExtension() { .operationContext(PRIMARY_CONTEXT) .build()) ) - .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + .withMessage("Attribute 'autogeneratedKeyField' cannot have both " + + "@DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } @Test - public void uuidExtensionFirst_detectsConflictWithKeyExtension() { - TestItem item = new TestItem(); - item.setId(RECORD_ID); + public void autogeneratedUuidExtensionFirst_detectsConflictWithKeyExtension() { + RecordWithAutogenerated item = new RecordWithAutogenerated(); + item.setAutogeneratedKeyField(RECORD_ID); Map items = CONFLICTING_SCHEMA.itemToMap(item, true); @@ -97,28 +98,29 @@ public void uuidExtensionFirst_detectsConflictWithKeyExtension() { .operationContext(PRIMARY_CONTEXT) .build()) ) - .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + .withMessage("Attribute 'autogeneratedKeyField' cannot have both " + + "@DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } @Test - public void separateAttributes_noConflict() { - // Schema with annotations on different attributes - should work fine - StaticTableSchema separateSchema = - StaticTableSchema.builder(TestItemSeparate.class) - .newItemSupplier(TestItemSeparate::new) - .addAttribute(String.class, a -> a.name("id") - .getter(TestItemSeparate::getId) - .setter(TestItemSeparate::setId) + public void autogeneratedAnnotations_onSeparateAttributes_doesNotDetectConflict() { + // Schema with annotations on different attributes - does not detect conflict + StaticTableSchema separateSchema = + StaticTableSchema.builder(RecordWithAutogenerated.class) + .newItemSupplier(RecordWithAutogenerated::new) + .addAttribute(String.class, a -> a.name("autogeneratedKeyField") + .getter(RecordWithAutogenerated::getAutogeneratedKeyField) + .setter(RecordWithAutogenerated::setAutogeneratedKeyField) .addTag(primaryPartitionKey()) .addTag(autoGeneratedKeyAttribute())) - .addAttribute(String.class, a -> a.name("uuidField") - .getter(TestItemSeparate::getUuidField) - .setter(TestItemSeparate::setUuidField) + .addAttribute(String.class, a -> a.name("autogeneratedUuidField") + .getter(RecordWithAutogenerated::getAutogeneratedUuidField) + .setter(RecordWithAutogenerated::setAutogeneratedUuidField) .addTag(autoGeneratedUuidAttribute())) .build(); - TestItemSeparate item = new TestItemSeparate(); + RecordWithAutogenerated item = new RecordWithAutogenerated(); Map items = separateSchema.itemToMap(item, true); @@ -138,36 +140,24 @@ public void separateAttributes_noConflict() { .build()); } - public static class TestItem { - private String id; + public static class RecordWithAutogenerated { + private String autogeneratedKeyField; + private String autogeneratedUuidField; - public String getId() { - return id; + public String getAutogeneratedKeyField() { + return autogeneratedKeyField; } - public void setId(String id) { - this.id = id; - } - } - - public static class TestItemSeparate { - private String id; - private String uuidField; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; + public void setAutogeneratedKeyField(String autogeneratedKeyField) { + this.autogeneratedKeyField = autogeneratedKeyField; } - public String getUuidField() { - return uuidField; + public String getAutogeneratedUuidField() { + return autogeneratedUuidField; } - public void setUuidField(String uuidField) { - this.uuidField = uuidField; + public void setAutogeneratedUuidField(String autogeneratedUuidField) { + this.autogeneratedUuidField = autogeneratedUuidField; } } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java index 81622bd9b495..e84378201b20 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java @@ -19,11 +19,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem.createUniqueFakeItem; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort.createUniqueFakeItemWithSort; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import java.util.HashMap; import java.util.Map; @@ -44,7 +41,6 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeVersionedStaticImmutableItem; import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext; -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.DynamoDbPartitionKey; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -269,30 +265,15 @@ public static Stream customStartAtAndIncrementValues() { @MethodSource("customFailingStartAtAndIncrementValues") public void customStartingValueAndIncrement_shouldThrow(Long startAt, Long incrementBy) { assertThrows(IllegalArgumentException.class, () -> VersionedRecordExtension.builder() - .startAt(startAt) - .incrementBy(incrementBy) - .build()); + .startAt(startAt) + .incrementBy(incrementBy) + .build()); } public static Stream customFailingStartAtAndIncrementValues() { return Stream.of( Arguments.of(-2L, 1L), - Arguments.of(3L, 0L), - Arguments.of(-1L, 0L)); - } - - @Test - public void builder_startAtValueIsLessThanMinusOne_throwsIllegalArgumentException() { - assertThrows(IllegalArgumentException.class, - () -> VersionedRecordExtension.builder().startAt(-2L).build(), - "startAt must be -1 or greater"); - } - - @Test - public void builder_incrementByValueIsLessThanOne_throwsIllegalArgumentException() { - assertThrows(IllegalArgumentException.class, - () -> VersionedRecordExtension.builder().incrementBy(0L).build(), - "incrementBy must be greater than 0."); + Arguments.of(3L, 0L)); } @Test @@ -394,9 +375,9 @@ public void customStartingValueAndIncrementWithAnnotation_worksAsExpected() { @Test public void customAnnotationValuesAndBuilderValues_annotationShouldTakePrecedence() { VersionedRecordExtension recordExtension = VersionedRecordExtension.builder() - .startAt(5L) - .incrementBy(2L) - .build(); + .startAt(5L) + .incrementBy(2L) + .build(); FakeVersionedThroughAnnotationItem item = new FakeVersionedThroughAnnotationItem(); item.setId(UUID.randomUUID().toString()); @@ -535,7 +516,7 @@ public void customIncrementForExistingVersion_worksAsExpected(Long startAt, Long @ParameterizedTest @MethodSource("customIncrementForExistingVersionValues") public void customIncrementForExistingVersion_withImmutableSchema_worksAsExpected(Long startAt, Long incrementBy, - Long existingVersion, String expectedNextVersion) { + Long existingVersion, String expectedNextVersion) { VersionedRecordExtension.Builder recordExtensionBuilder = VersionedRecordExtension.builder(); if (startAt != null) { recordExtensionBuilder.startAt(startAt); @@ -700,89 +681,4 @@ public static Stream customIncrementForExistingVersionValues() { Arguments.of(3L, null, 10L, "11"), Arguments.of(null, 3L, 4L, "7")); } - - @Test - public void versionAttribute_withInvalidStartAt_throwsIllegalArgumentException() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> - StaticTableSchema.builder(TestItem.class) - .newItemSupplier(TestItem::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(TestItem::getId) - .setter(TestItem::setId) - .addTag(primaryPartitionKey())) - .addAttribute(Long.class, - a -> a.name("version") - .getter(TestItem::getVersion) - .setter(TestItem::setVersion) - .addTag(versionAttribute(-2L, 1L))) - .build() - ) - .withMessage("startAt must be -1 or greater."); - } - - @Test - public void versionAttribute_withInvalidIncrementBy_throwsIllegalArgumentException() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> - StaticTableSchema.builder(TestItem.class) - .newItemSupplier(TestItem::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(TestItem::getId) - .setter(TestItem::setId) - .addTag(primaryPartitionKey())) - .addAttribute(Long.class, - a -> a.name("version") - .getter(TestItem::getVersion) - .setter(TestItem::setVersion) - .addTag(versionAttribute(0L, 0L))) - .build() - ) - .withMessage("incrementBy must be greater than 0."); - } - - @Test - public void versionAttribute_withNonNumericType_throwsIllegalArgumentException() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> - StaticTableSchema.builder(TestItem.class) - .newItemSupplier(TestItem::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(TestItem::getId) - .setter(TestItem::setId) - .addTag(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("version") - .getter(TestItem::getId) - .setter(TestItem::setId) - .addTag(versionAttribute())) - .build() - ) - .withMessageContaining( - "is not a suitable type to be used as a version attribute. Only type 'N' is supported."); - } - - private static class TestItem { - private String id; - private Long version; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } - } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java index 506789cadd0c..ff811115937b 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java @@ -48,9 +48,12 @@ /** * Tests for @DynamoDbAutoGeneratedKey annotation functionality. *

- * Tests cover: - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK) - UpdateBehavior control (WRITE_ALWAYS vs - * WRITE_IF_NOT_EXISTS) for secondary index keys - Primary key limitations (UpdateBehavior has no effect) - Error handling for - * invalid usage - Integration with other extensions (VersionedRecord) + * Tests cover: + * - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK) + * - UpdateBehavior control (WRITE_ALWAYS vs WRITE_IF_NOT_EXISTS) for secondary index keys + * - Primary key limitations (UpdateBehavior has no effect) + * - Error handling for invalid usage + * - Integration with other extensions (VersionedRecord) */ @RunWith(Parameterized.class) public class AutoGeneratedKeyRecordTest extends LocalDynamoDbSyncTestBase { @@ -116,7 +119,6 @@ public void deleteTable() { @Test public void putItem_generatesUuidsForAllFourKeyTypes() { TestRecord record = new TestRecord(); - // Don't set any keys - they should all be auto-generated mappedTable.putItem(record); TestRecord retrieved = mappedTable.scan().items().stream().findFirst() @@ -128,7 +130,7 @@ public void putItem_generatesUuidsForAllFourKeyTypes() { assertValidUuid(retrieved.getGsiPk()); // GSI partition key assertValidUuid(retrieved.getGsiSk()); // GSI sort key - // Verify they're all different + // Verify they're all unique keys assertThat(retrieved.getId()).isNotEqualTo(retrieved.getSortKey()); assertThat(retrieved.getGsiPk()).isNotEqualTo(retrieved.getGsiSk()); assertThat(retrieved.getId()).isNotEqualTo(retrieved.getGsiPk()); @@ -136,7 +138,7 @@ public void putItem_generatesUuidsForAllFourKeyTypes() { @Test public void updateItem_respectsUpdateBehaviorForSecondaryIndexKeys() { - // Put initial record + // Put record TestRecord record = new TestRecord(); mappedTable.putItem(record); @@ -145,7 +147,7 @@ public void updateItem_respectsUpdateBehaviorForSecondaryIndexKeys() { String id = afterPut.getId(); String sortKey = afterPut.getSortKey(); String originalGsiPk = afterPut.getGsiPk(); // WRITE_ALWAYS (default) → should change - String originalGsiSk = afterPut.getGsiSk(); // WRITE_IF_NOT_EXISTS → should preserve + String originalGsiSk = afterPut.getGsiSk(); // WRITE_IF_NOT_EXISTS → should preserve // Update record TestRecord updateRecord = new TestRecord(); @@ -161,7 +163,7 @@ public void updateItem_respectsUpdateBehaviorForSecondaryIndexKeys() { } @Test - public void nonKeyAttribute_throwsException() { + public void putItem_withAutogeneratedKeySetOnNonKeyAttribute_throwsException() { String tableName = getConcreteTableName("invalid-usage-test"); DynamoDbEnhancedClient client = createClient(); @@ -231,7 +233,7 @@ public void versionedRecord_worksWithAutoGeneratedKeys() { } @Test - public void conflictingAnnotations_throwsException() { + public void autogeneratedConflictingAnnotations_throwsException() { String tableName = getConcreteTableName("conflicting-annotations-test"); DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java index 3bdc7a5eb218..d513df433c35 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java @@ -312,8 +312,8 @@ public void updateItemConditionTestFailure() { } @Test - public void conflictingAnnotations_throwsException() { - String tableName = getConcreteTableName("conflicting-annotations-test"); + public void putItem_onRecordWithAutogeneratedConflictingAnnotations_throwsException() { + String tableName = getConcreteTableName("conflicting-annotations-record-table"); DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) @@ -326,7 +326,7 @@ public void conflictingAnnotations_throwsException() { table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); ConflictingAnnotationsRecord record = new ConflictingAnnotationsRecord(); - record.setPayload("test"); + record.setPayload("payload"); Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> table.putItem(record)) @@ -508,8 +508,8 @@ public static class ConflictingAnnotationsRecord { // Both annotations on the same field - should cause an exception @DynamoDbPartitionKey - @DynamoDbAutoGeneratedUuid @DynamoDbAutoGeneratedKey + @DynamoDbAutoGeneratedUuid public String getId() { return id; } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java index 52f80818216e..6e24fe2d96e0 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java @@ -78,6 +78,7 @@ public void putItem_whenKeysNotPopulated_generatesNewUuids() { mappedTable.putItem(record); AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst() .orElseThrow(() -> new AssertionError("No record found")); + isValidUuid(result.getId()); isValidUuid(result.getSortKey()); isValidUuid(result.getGsiPk()); @@ -137,15 +138,15 @@ public void updateItem_respectsUpdateBehavior() { } @Test - public void batchWrite_whenKeysNotPopulated_generatesNewUuids() { - AutogeneratedKeyRecord record1 = new AutogeneratedKeyRecord(); - AutogeneratedKeyRecord record2 = new AutogeneratedKeyRecord(); + public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { + AutogeneratedKeyRecord firstRecord = new AutogeneratedKeyRecord(); + AutogeneratedKeyRecord secondRecord = new AutogeneratedKeyRecord(); enhancedClient.batchWriteItem(req -> req.addWriteBatch( WriteBatch.builder(AutogeneratedKeyRecord.class) .mappedTableResource(mappedTable) - .addPutItem(record1) - .addPutItem(record2) + .addPutItem(firstRecord) + .addPutItem(secondRecord) .build())); List results = mappedTable.scan().items().stream().collect(Collectors.toList()); @@ -158,23 +159,23 @@ public void batchWrite_whenKeysNotPopulated_generatesNewUuids() { @Test public void batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { - AutogeneratedKeyRecord record1 = new AutogeneratedKeyRecord(); - record1.setId("existing-id-1"); - record1.setSortKey("existing-sk-1"); - record1.setGsiPk("existing-gsiPk-1"); - record1.setGsiSk("existing-gsiSk-1"); - - AutogeneratedKeyRecord record2 = new AutogeneratedKeyRecord(); - record2.setId("existing-id-2"); - record2.setSortKey("existing-sk-2"); - record2.setGsiPk("existing-gsiPk-2"); - record2.setGsiSk("existing-gsiSk-2"); + AutogeneratedKeyRecord firstRecord = new AutogeneratedKeyRecord(); + firstRecord.setId("existing-id-1"); + firstRecord.setSortKey("existing-sk-1"); + firstRecord.setGsiPk("existing-gsiPk-1"); + firstRecord.setGsiSk("existing-gsiSk-1"); + + AutogeneratedKeyRecord secondRecord = new AutogeneratedKeyRecord(); + secondRecord.setId("existing-id-2"); + secondRecord.setSortKey("existing-sk-2"); + secondRecord.setGsiPk("existing-gsiPk-2"); + secondRecord.setGsiSk("existing-gsiSk-2"); enhancedClient.batchWriteItem(req -> req.addWriteBatch( WriteBatch.builder(AutogeneratedKeyRecord.class) .mappedTableResource(mappedTable) - .addPutItem(record1) - .addPutItem(record2) + .addPutItem(firstRecord) + .addPutItem(secondRecord) .build())); AutogeneratedKeyRecord savedRecord1 = @@ -193,7 +194,7 @@ public void batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { } @Test - public void transactWrite_whenKeysNotPopulated_generatesNewUuids() { + public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); enhancedClient.transactWriteItems( diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java index 03588232bd2b..c9a29f0e4563 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java @@ -70,13 +70,14 @@ public void deleteTable() { } @Test - public void putItem_whenKeysNotPopulated_generatesNewUuids() { + public void putItem_whenKeysNotAlreadyPopulated_generatesNewUuids() { RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); record.setId("existing-id"); mappedTable.putItem(record); RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() .orElseThrow(() -> new AssertionError("No record found")); + isValidUuid(result.getId()); isValidUuid(result.getSortKey()); } @@ -98,7 +99,7 @@ public void putItem_whenKeysAlreadyPopulated_replacesExistingUuids() { } @Test - public void batchWrite_whenKeysNotPopulated_generatesNewUuids() { + public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { RecordWithAutogeneratedUuid record1 = new RecordWithAutogeneratedUuid(); RecordWithAutogeneratedUuid record2 = new RecordWithAutogeneratedUuid(); @@ -149,7 +150,7 @@ public void batchWrite_whenKeysAlreadyPopulated_generatesNewUuids() { } @Test - public void transactWrite_whenKeysNotPopulated_generatesNewUuids() { + public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { RecordWithAutogeneratedUuid record = new RecordWithAutogeneratedUuid(); enhancedClient.transactWriteItems( @@ -183,7 +184,7 @@ public void transactWrite_whenKeysAlreadyPopulated_generatesNewUuids() { } @Test - public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_doesNotRegenerateUuids() { + public void putItem_whenAutogeneratedUuidAnnotationIsNotPresent_doesNotRegenerateUuids() { String tableName = "no-autogenerated-uuid-table"; DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(RecordWithoutAutogeneratedUuid.class)); @@ -201,6 +202,7 @@ public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_doesNotRegenerate mappedTable.putItem(record); RecordWithoutAutogeneratedUuid retrieved = mappedTable.getItem( r -> r.key(k -> k.partitionValue("existing-id").sortValue("existing-sk"))); + assertThat(retrieved.getId()).isEqualTo("existing-id"); assertThat(retrieved.getSortKey()).isEqualTo("existing-sk"); assertThat(retrieved.getGsiPk()).isEqualTo("existing-gsiPk"); @@ -215,7 +217,7 @@ public void putItem_whenNoAutogeneratedUuidAnnotationIsPresent_doesNotRegenerate } @Test - public void createBean_givenAutogeneratedUuidAnnotationAppliedOnNonStringAttributeType_throwsException() { + public void createBean_whenAutogeneratedUuidAnnotationIsAppliedOnNonStringAttribute_throwsException() { assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedUuidInvalidTypeRecord.class)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Uuid attribute") From 08012f9da054851633a7aa8500969c843016ae82 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Thu, 12 Feb 2026 15:40:53 +0200 Subject: [PATCH 07/14] Tests refactoring --- .../extensions/AutoGeneratedKeyExtension.java | 2 +- .../AutoGeneratedKeyExtensionTest.java | 150 +++++++++--------- .../AutoGeneratedUuidExtensionTest.java | 5 +- .../ConflictingAnnotationsTest.java | 6 +- .../AutoGeneratedKeyRecordTest.java | 21 ++- .../AutoGeneratedUuidRecordTest.java | 11 +- .../AutoGeneratedKeyExtensionTest.java | 14 +- .../AutoGeneratedUuidExtensionTest.java | 5 +- 8 files changed, 111 insertions(+), 103 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java index 9db27b1ff6d8..d09e7bc47174 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java @@ -139,7 +139,7 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex .ifPresent(attr -> { throw new IllegalArgumentException( "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " - + "primary partition key, primary sort key, or GSI/LSI partition/sort keys." + + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. " + "Invalid placement on attribute: " + attr); }); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java index 5b57765872c6..dd059c9a271c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java @@ -109,13 +109,13 @@ public void updateItem_withExistingKey_preservesValueAndDoesNotGenerateNewOne() Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); assertThat(items).hasSize(2); - WriteModification result = - extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) - .operationName(OperationName.UPDATE_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build()); + WriteModification result = extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) + .operationName(OperationName.UPDATE_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); Map transformed = result.transformedItem(); assertThat(transformed).isNotNull().hasSize(2); @@ -133,13 +133,13 @@ public void updateItem_withoutExistingKey_generatesNewUuid() { Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); assertThat(items).hasSize(1); - WriteModification result = - extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) - .operationName(OperationName.UPDATE_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build()); + WriteModification result = extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) + .operationName(OperationName.UPDATE_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); Map transformed = result.transformedItem(); assertThat(transformed).isNotNull().hasSize(2); @@ -156,13 +156,13 @@ public void updateItem_withMissingKeyAttribute_insertsGeneratedUuid() { Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); assertThat(items).hasSize(1); - WriteModification result = - extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) - .operationName(OperationName.UPDATE_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build()); + WriteModification result = extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) + .operationName(OperationName.UPDATE_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); assertThat(result.transformedItem()).isNotNull(); assertThat(result.updateExpression()).isNull(); @@ -189,8 +189,9 @@ public void nonStringAttributeAnnotatedWithAutoGeneratedKey_throwsIllegalArgumen .setter(AutogeneratedKeyItem::setSimpleString)) .build() ) - .withMessage("Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type " - + "to be used as a Auto Generated Key attribute. Only String Class type is supported."); + .withMessage( + "Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type " + + "to be used as a Auto Generated Key attribute. Only String Class type is supported."); } @Test @@ -200,13 +201,13 @@ public void autoGeneratedKey_onSecondaryPartitionKey_generatesUuid() { Map items = ITEM_WITH_KEY_SCHEMA.itemToMap(item, true); - WriteModification result = - extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) - .operationName(OperationName.PUT_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build()); + WriteModification result = extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(ITEM_WITH_KEY_SCHEMA.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); Map transformed = result.transformedItem(); assertThat(transformed).isNotNull(); @@ -220,13 +221,13 @@ public void autoGeneratedKey_onSecondarySortKey_generatesUuid() { Map items = LSI_SK_AUTOGEN_SCHEMA.itemToMap(item, true); - WriteModification result = - extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(LSI_SK_AUTOGEN_SCHEMA.tableMetadata()) - .operationName(OperationName.PUT_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build()); + WriteModification result = extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(LSI_SK_AUTOGEN_SCHEMA.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()); Map transformed = result.transformedItem(); assertThat(transformed).isNotNull(); @@ -241,13 +242,13 @@ public void autoGeneratedKey_onNonKeyAttribute_throwsIllegalArgumentException() Map items = INVALID_NONKEY_AUTOGEN_SCHEMA.itemToMap(item, true); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> - extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(INVALID_NONKEY_AUTOGEN_SCHEMA.tableMetadata()) - .operationName(OperationName.PUT_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build()) + .isThrownBy(() -> extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(INVALID_NONKEY_AUTOGEN_SCHEMA.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()) ) .withMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes: " + "primary partition key, primary sort key, or GSI/LSI partition/sort keys.") @@ -279,16 +280,17 @@ public void conflictingAnnotations_onSameAttribute_throwsIllegalArgumentExceptio Map items = conflictingSchema.itemToMap(item, true); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> - extension.beforeWrite(DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(conflictingSchema.tableMetadata()) - .operationName(OperationName.PUT_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build()) + .isThrownBy(() -> extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(conflictingSchema.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build()) ) - .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " - + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + .withMessage( + "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } @Test @@ -320,17 +322,16 @@ public void conflictingAnnotations_onSecondaryKey_throwsIllegalArgumentException Map items = conflictingGsiSchema.itemToMap(item, true); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> - extension.beforeWrite( - DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(conflictingGsiSchema.tableMetadata()) - .operationName(OperationName.PUT_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build())) - .withMessage("Attribute 'keyAttribute' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " - + "annotations. " - + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + .isThrownBy(() -> extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(conflictingGsiSchema.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build())) + .withMessage( + "Attribute 'keyAttribute' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } @Test @@ -354,17 +355,16 @@ public void conflictDetection_worksRegardlessOfExtensionOrder() { // Test that the conflict is detected regardless of which extension runs first assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> - extension.beforeWrite( - DefaultDynamoDbExtensionContext.builder() - .items(items) - .tableMetadata(conflictingSchema.tableMetadata()) - .operationName(OperationName.PUT_ITEM) - .operationContext(PRIMARY_CONTEXT) - .build())) - .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " - + "annotations. " - + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + .isThrownBy(() -> extension.beforeWrite( + DefaultDynamoDbExtensionContext.builder() + .items(items) + .tableMetadata(conflictingSchema.tableMetadata()) + .operationName(OperationName.PUT_ITEM) + .operationContext(PRIMARY_CONTEXT) + .build())) + .withMessage( + "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java index d506fced8d6f..33da228c5472 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtensionTest.java @@ -180,8 +180,9 @@ void IllegalArgumentException_for_AutogeneratedUuid_withNonStringType() { .setter(ItemWithUuid::setSimpleString)) .build()) - .withMessage("Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type" - + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported."); + .withMessage( + "Attribute 'intAttribute' of Class type class java.lang.Integer is not a suitable Java Class type" + + " to be used as a Auto Generated Uuid attribute. Only String Class type is supported."); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java index 7b7d01c7afce..456650ec706f 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java @@ -76,9 +76,9 @@ public void autogeneratedKeyExtensionFirst_detectsConflictWithUuidExtension() { .operationContext(PRIMARY_CONTEXT) .build()) ) - .withMessage("Attribute 'autogeneratedKeyField' cannot have both " - + "@DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " - + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + .withMessage( + "Attribute 'autogeneratedKeyField' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " + + "annotations. These annotations have conflicting behaviors and cannot be used together on the same attribute."); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java index ff811115937b..f2b3283b0650 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java @@ -48,12 +48,9 @@ /** * Tests for @DynamoDbAutoGeneratedKey annotation functionality. *

- * Tests cover: - * - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK) - * - UpdateBehavior control (WRITE_ALWAYS vs WRITE_IF_NOT_EXISTS) for secondary index keys - * - Primary key limitations (UpdateBehavior has no effect) - * - Error handling for invalid usage - * - Integration with other extensions (VersionedRecord) + * Tests cover: - Basic UUID generation on all 4 key types (primary PK/SK, GSI PK/SK) - UpdateBehavior control (WRITE_ALWAYS vs + * WRITE_IF_NOT_EXISTS) for secondary index keys - Primary key limitations (UpdateBehavior has no effect) - Error handling for + * invalid usage - Integration with other extensions (VersionedRecord) */ @RunWith(Parameterized.class) public class AutoGeneratedKeyRecordTest extends LocalDynamoDbSyncTestBase { @@ -189,8 +186,10 @@ public void putItem_withAutogeneratedKeySetOnNonKeyAttribute_throwsException() { Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> table.putItem(record)) - .withMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes") - .withMessageContaining("notAKey"); + .withMessage( + "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " + + "primary partition key, primary sort key, or GSI/LSI partition/sort keys." + + "Invalid placement on attribute: notAKey"); } finally { deleteTableByName(tableName); } @@ -252,9 +251,9 @@ public void autogeneratedConflictingAnnotations_throwsException() { Assertions .assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> table.putItem(record)) - .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " - + "annotations. " - + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + .withMessage( + "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } finally { deleteTableByName(tableName); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java index d513df433c35..161cf7b30fd5 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension.AttributeTags.autoGeneratedUuidAttribute; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; @@ -328,11 +329,11 @@ public void putItem_onRecordWithAutogeneratedConflictingAnnotations_throwsExcept ConflictingAnnotationsRecord record = new ConflictingAnnotationsRecord(); record.setPayload("payload"); - Assertions.assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> table.putItem(record)) - .withMessage("Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " - + "annotations. These annotations have conflicting behaviors and cannot be used together " - + "on the same attribute."); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> table.putItem(record)) + .withMessage( + "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } finally { getDynamoDbClient().deleteTable(DeleteTableRequest.builder().tableName(tableName).build()); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java index 6e24fe2d96e0..2e1ca89a0830 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java @@ -278,7 +278,9 @@ public void putItem_whenAnnotationInConflictWithAutogeneratedUuidAnnotation_thro assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyConflictingRecord())) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid"); + .hasMessage( + "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); } finally { try { mappedTable.deleteTable(); @@ -298,7 +300,10 @@ public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() { assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyOnNonKeyAttributeRecord())) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes"); + .hasMessage( + "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " + + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. " + + "Invalid placement on attribute: nonKeyAttribute"); } finally { try { mappedTable.deleteTable(); @@ -342,8 +347,9 @@ public void putItem_whenNoAutogeneratedKeyAnnotationIsPresent_doesNotRegenerateU public void createBean_givenAutogeneratedKeyAnnotationAppliedOnNonStringAttributeType_throwsException() { assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedKeyInvalidTypeRecord.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Key attribute") - .hasMessageContaining("Only String Class type is supported"); + .hasMessage( + "Attribute 'id' of Class type class java.lang.Integer is not a suitable Java Class type to be used " + + "as a Auto Generated Key attribute. Only String Class type is supported."); } @DynamoDbBean diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java index c9a29f0e4563..974d638ed8d9 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java @@ -220,8 +220,9 @@ public void putItem_whenAutogeneratedUuidAnnotationIsNotPresent_doesNotRegenerat public void createBean_whenAutogeneratedUuidAnnotationIsAppliedOnNonStringAttribute_throwsException() { assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedUuidInvalidTypeRecord.class)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("not a suitable Java Class type to be used as a Auto Generated Uuid attribute") - .hasMessageContaining("Only String Class type is supported"); + .hasMessage( + "Attribute 'id' of Class type class java.lang.Integer is not a suitable Java Class type to be used " + + "as a Auto Generated Uuid attribute. Only String Class type is supported."); } @DynamoDbBean From 67cd161eb9cb71f3c5c654761c7927350dbe6a0f Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 13 Feb 2026 10:32:03 +0200 Subject: [PATCH 08/14] Addressed PR feedback and refactored tests --- .../extensions/AutoGeneratedKeyExtension.java | 172 +++++++++++------- .../AutoGeneratedUuidExtension.java | 51 ++++-- .../extensions/ExtensionsValidationUtils.java | 74 ++++++++ .../AutoGeneratedKeyExtensionTest.java | 8 +- ...ogeneratedConflictingAnnotationsTest.java} | 4 +- .../ExtensionsValidationUtilsTest.java | 92 ++++++++++ .../AutoGeneratedKeyRecordTest.java | 16 +- .../AutoGeneratedUuidRecordTest.java | 2 +- .../AutoGeneratedKeyExtensionTest.java | 2 +- 9 files changed, 321 insertions(+), 100 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/{ConflictingAnnotationsTest.java => AutogeneratedConflictingAnnotationsTest.java} (99%) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtilsTest.java diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java index d09e7bc47174..43a1943b521a 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.extensions; +import static java.util.Collections.newSetFromMap; + import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -23,6 +25,7 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -39,65 +42,86 @@ import software.amazon.awssdk.utils.Validate; /** - * Generates a random UUID (via {@link java.util.UUID#randomUUID()}) for any attribute tagged with - * {@code @DynamoDbAutoGeneratedKey} when that attribute is missing or empty on a write (put/update). - *

- * Key Difference from @DynamoDbAutoGeneratedUuid: This extension only generates UUIDs when the - * attribute value is null or empty, preserving existing values. In contrast, {@code @DynamoDbAutoGeneratedUuid} always generates - * new UUIDs regardless of existing values. - *

- * Conflict Detection: This extension cannot be used together with {@code @DynamoDbAutoGeneratedUuid} on the same - * attribute. If both annotations are applied to the same field, an {@link IllegalArgumentException} will be thrown at runtime to - * prevent unpredictable behavior based on extension load order. - *

- * The annotation may be placed only on key attributes: + * Generates a random UUID (via {@link java.util.UUID#randomUUID()}) for attributes tagged with {@code @DynamoDbAutoGeneratedKey} + * when the attribute value is missing or empty during write operations (put, update, batch write, or transact write). + * + *

Difference from {@code @DynamoDbAutoGeneratedUuid}: + * This extension generates a UUID only when the attribute is null or empty, preserving existing values. In contrast, + * {@code @DynamoDbAutoGeneratedUuid} always generates a new UUID regardless of the current value. + * + *

Conflict Detection: + * {@code @DynamoDbAutoGeneratedKey} and {@code @DynamoDbAutoGeneratedUuid} cannot be applied to the same attribute. If both + * annotations are present, an {@link IllegalArgumentException} is thrown during schema validation. + * + *

Supported Attributes: + * The annotation may only be applied to key attributes: *

    - *
  • Primary partition key (PK) or primary sort key (SK)
  • - *
  • Partition key or sort key of any secondary index (GSI or LSI)
  • + *
  • Primary partition key (PK)
  • + *
  • Primary sort key (SK)
  • + *
  • Partition or sort keys of secondary indexes (GSI or LSI)
  • *
* - *

Validation: The extension enforces this at runtime during {@link #beforeWrite} by comparing the - * annotated attributes against the table's known key attributes. If an annotated attribute - * is not a PK/SK or an GSI/LSI, an {@link IllegalArgumentException} is thrown.

+ *

Validation Behavior: + * Annotation conflict detection and key-placement validation are performed once per {@link TableMetadata} + * instance and cached to avoid repeated validation on subsequent writes. * - *

UpdateBehavior Limitations: {@code @DynamoDbUpdateBehavior} has no effect on primary keys due to - * DynamoDB's UpdateItem API requirements. It only affects secondary index keys.

+ *

UpdateBehavior Limitation: + * {@code @DynamoDbUpdateBehavior} does not apply to primary keys due to DynamoDB API constraints. + * It only affects secondary index keys. */ @SdkPublicApi @ThreadSafe public final class AutoGeneratedKeyExtension implements DynamoDbEnhancedClientExtension { /** - * Custom metadata key under which we store the set of annotated attribute names. + * Metadata keys used to record attributes annotated with {@code @DynamoDbAutoGeneratedKey} and + * {@code @DynamoDbAutoGeneratedUuid}. These are used during schema validation to detect annotation conflicts. */ private static final String CUSTOM_METADATA_KEY = "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute"; + private static final String UUID_EXTENSION_METADATA_KEY = + "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; + + private static final StaticAttributeTag AUTO_GENERATED_KEY_ATTRIBUTE = new AutoGeneratedKeyAttribute(); + private static final String AUTOGENERATED_KEY_ANNOTATION = "@DynamoDbAutoGeneratedKey"; + private static final String AUTOGENERATED_UUID_ANNOTATION = "@DynamoDbAutoGeneratedUuid"; /** - * Metadata key used by AutoGeneratedUuidExtension to detect conflicts. + * Stores the TableMetadata instances that have already been validated by this extension. Uses a ConcurrentHashMap to ensure + * thread-safe access during concurrent write operations. */ - private static final String UUID_EXTENSION_METADATA_KEY = - "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; + private final Set validatedSchemas = newSetFromMap(new ConcurrentHashMap<>()); - private static final AutoGeneratedKeyAttribute AUTO_GENERATED_KEY_ATTRIBUTE = new AutoGeneratedKeyAttribute(); + /** + * Caches the set of valid key attribute names per TableMetadata instance. Computed once per schema. + */ + private final Map> allowedKeysCache = new ConcurrentHashMap<>(); private AutoGeneratedKeyExtension() { } + /** + * @return an Instance of {@link AutoGeneratedKeyExtension} + */ + public static AutoGeneratedKeyExtension create() { + return new AutoGeneratedKeyExtension(); + } + public static Builder builder() { return new Builder(); } /** - * If this table has attributes tagged for auto-generation, insert a UUID value into the outgoing item for any such attribute - * that is currently missing/empty. Unlike {@code @DynamoDbAutoGeneratedUuid}, this preserves existing values. - *

- * Also validates that the annotation is only used on PK/SK/GSI/LSI key attributes and that there are no conflicts with + * Inserts a UUID for attributes tagged with {@code @DynamoDbAutoGeneratedKey} when the attribute is null or empty, preserving + * existing values. * - * @DynamoDbAutoGeneratedUuid. + *

Schema-level validation (annotation conflict detection and key-placement checks) + * is executed once per schema instance and cached. */ @Override public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) { + + Collection taggedAttributes = context.tableMetadata() .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class) .orElse(null); @@ -106,44 +130,12 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex return WriteModification.builder().build(); } - // Check for conflicts with @DynamoDbAutoGeneratedUuid - Collection uuidTaggedAttributes = context.tableMetadata() - .customMetadataObject(UUID_EXTENSION_METADATA_KEY, Collection.class) - .orElse(Collections.emptyList()); - - taggedAttributes.stream() - .filter(uuidTaggedAttributes::contains) - .findFirst() - .ifPresent(attribute -> { - throw new IllegalArgumentException( - "Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and " - + "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors " - + "and cannot be used together on the same attribute."); - }); - - TableMetadata metadata = context.tableMetadata(); - Set allowedKeys = new HashSet<>(); - - // ensure every @DynamoDbAutoGeneratedKey attribute is a PK/SK or GSI/LSI. If not, throw IllegalArgumentException - allowedKeys.add(metadata.primaryPartitionKey()); - metadata.primarySortKey().ifPresent(allowedKeys::add); - - metadata.indices().stream().map(IndexMetadata::name).forEach(indexName -> { - allowedKeys.add(metadata.indexPartitionKey(indexName)); - metadata.indexSortKey(indexName).ifPresent(allowedKeys::add); - }); + TableMetadata tableMetadata = context.tableMetadata(); + if (validatedSchemas.add(tableMetadata)) { + validateNoAutoGeneratedAnnotationConflict(tableMetadata); + validateAutoGeneratedKeyPlacement(tableMetadata, taggedAttributes); + } - taggedAttributes.stream() - .filter(attr -> !allowedKeys.contains(attr)) - .findFirst() - .ifPresent(attr -> { - throw new IllegalArgumentException( - "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " - + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. " - + "Invalid placement on attribute: " + attr); - }); - - // Generate UUIDs for missing/empty annotated attributes Map itemToTransform = new HashMap<>(context.items()); taggedAttributes.forEach(attr -> insertUuidIfMissing(itemToTransform, attr)); @@ -152,6 +144,52 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex .build(); } + /** + * Validates (once per TableMetadata instance) that @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid are not applied + * to the same attribute. + */ + private static void validateNoAutoGeneratedAnnotationConflict(TableMetadata tableMetadata) { + ExtensionsValidationUtils.validateNoAnnotationConflict( + tableMetadata, + CUSTOM_METADATA_KEY, + UUID_EXTENSION_METADATA_KEY, + AUTOGENERATED_KEY_ANNOTATION, + AUTOGENERATED_UUID_ANNOTATION + ); + } + + /** + * Validates that all attributes tagged with @DynamoDbAutoGeneratedKey are either primary keys or secondary index keys. + **/ + private void validateAutoGeneratedKeyPlacement(TableMetadata tableMetadata, + Collection taggedAttributeNames) { + + Set allowedKeyAttributes = allowedKeysCache.computeIfAbsent(tableMetadata, metadata -> { + Set keyAttributes = new HashSet<>(); + keyAttributes.add(metadata.primaryPartitionKey()); + metadata.primarySortKey().ifPresent(keyAttributes::add); + + metadata.indices().stream() + .map(IndexMetadata::name) + .forEach(indexName -> { + keyAttributes.add(metadata.indexPartitionKey(indexName)); + metadata.indexSortKey(indexName).ifPresent(keyAttributes::add); + }); + + return keyAttributes; + }); + + taggedAttributeNames.stream() + .filter(attrName -> !allowedKeyAttributes.contains(attrName)) + .findFirst() + .ifPresent(invalidAttribute -> { + throw new IllegalArgumentException( + "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " + + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. " + + "Invalid placement on attribute: " + invalidAttribute); + }); + } + private void insertUuidIfMissing(Map itemToTransform, String key) { AttributeValue existing = itemToTransform.get(key); if (Objects.isNull(existing) || StringUtils.isBlank(existing.s())) { @@ -210,7 +248,7 @@ public void validateType(String attributeName, @Override public Consumer modifyMetadata(String attributeName, AttributeValueType attributeValueType) { - // Record the names of the attributes annotated with @DynamoDbAutoGeneratedKey for later lookup in beforeWrite() + // Records attribute names annotated with @DynamoDbAutoGeneratedKey for lookup during write processing. return metadata -> metadata.addCustomMetadataObject( CUSTOM_METADATA_KEY, Collections.singleton(attributeName)); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java index 884e12c20f89..e3f7b8922d07 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java @@ -15,11 +15,15 @@ package software.amazon.awssdk.enhanced.dynamodb.extensions; +import static java.util.Collections.newSetFromMap; + import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -27,6 +31,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; @@ -85,17 +90,24 @@ @SdkPublicApi @ThreadSafe public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientExtension { - private static final String CUSTOM_METADATA_KEY = - "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; /** - * Metadata key used by AutoGeneratedKeyExtension to detect conflicts. + * Custom metadata keys under which AutoGeneratedUuidExtension/AutoGeneratedKeyExtension record attributes annotated with + * {@code @DynamoDbAutoGeneratedUuid}/{@code @DynamoDbAutoGeneratedKey}. Used to detect conflicts during schema validation. */ - private static final String KEY_EXTENSION_METADATA_KEY = + private static final String CUSTOM_METADATA_KEY = + "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; + private static final String AUTOGENERATED_KEY_EXTENSION_METADATA_KEY = "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute"; private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute(); + /** + * Stores the TableMetadata instances that have already been validated by this extension. Uses a ConcurrentHashMap to ensure + * thread-safe access during concurrent write operations. + */ + private final Set validatedSchemas = newSetFromMap(new ConcurrentHashMap<>()); + private AutoGeneratedUuidExtension() { } @@ -124,20 +136,8 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex return WriteModification.builder().build(); } - // Check for conflicts with @DynamoDbAutoGeneratedKey - Collection keyTaggedAttributes = context.tableMetadata() - .customMetadataObject(KEY_EXTENSION_METADATA_KEY, Collection.class) - .orElse(Collections.emptyList()); - - customMetadataObject.stream() - .filter(keyTaggedAttributes::contains) - .findFirst() - .ifPresent(attribute -> { - throw new IllegalArgumentException( - "Attribute '" + attribute + "' cannot have both @DynamoDbAutoGeneratedKey and " - + "@DynamoDbAutoGeneratedUuid annotations. These annotations have conflicting behaviors " - + "and cannot be used together on the same attribute."); - }); + TableMetadata metadata = context.tableMetadata(); + validateNoAnnotationConflict(metadata); Map itemToTransform = new HashMap<>(context.items()); customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key)); @@ -146,6 +146,21 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex .build(); } + /** + * Validates (once per TableMetadata instance) that @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid are not applied + * to the same attribute. + */ + private void validateNoAnnotationConflict(TableMetadata tableMetadata) { + if (validatedSchemas.add(tableMetadata)) { + ExtensionsValidationUtils.validateNoAnnotationConflict( + tableMetadata, + AUTOGENERATED_KEY_EXTENSION_METADATA_KEY, + CUSTOM_METADATA_KEY, + "@DynamoDbAutoGeneratedKey", + "@DynamoDbAutoGeneratedUuid"); + } + } + private void insertUuidInItemToTransform(Map itemToTransform, String key) { itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build()); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java new file mode 100644 index 000000000000..1b7a81e2ed6a --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.extensions; + +import java.util.Collection; +import java.util.Collections; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; + +/** + * Provides shared schema validation utilities for DynamoDB enhanced client extensions. + */ +@SdkProtectedApi +public final class ExtensionsValidationUtils { + + private ExtensionsValidationUtils() { + } + + /** + * Validates that there are no attributes that have both annotations. These annotations have conflicting behaviors and cannot + * be used together on the same attribute. If an attribute is found with both annotations, an IllegalArgumentException is + * thrown with a message indicating the attribute and the conflicting annotations. + * + * @param tableMetadata The metadata of the table to validate. + * @param firstMetadataKey The metadata key for the first annotation to check for. + * @param secondMetadataKey The metadata key for the second annotation to check for. + * @param firstAnnotationName The name of the first annotation to use in the error message if a conflict is found. + * @param secondAnnotationName The name of the second annotation to use in the error message if a conflict is found. + */ + public static void validateNoAnnotationConflict(TableMetadata tableMetadata, + String firstMetadataKey, + String secondMetadataKey, + String firstAnnotationName, + String secondAnnotationName) { + + Collection attributesWithFirstAnnotation = + tableMetadata.customMetadataObject(firstMetadataKey, Collection.class).orElse(Collections.emptyList()); + + if (attributesWithFirstAnnotation.isEmpty()) { + return; + } + + Collection attributesWithSecondAnnotation = + tableMetadata.customMetadataObject(secondMetadataKey, Collection.class).orElse(Collections.emptyList()); + + if (attributesWithSecondAnnotation.isEmpty()) { + return; + } + + attributesWithFirstAnnotation + .stream() + .filter(attributesWithSecondAnnotation::contains) + .findFirst() + .ifPresent(attribute -> { + throw new IllegalArgumentException( + "Attribute '" + attribute + "' cannot have both " + firstAnnotationName + + " and " + secondAnnotationName + " annotations. " + + "These annotations have conflicting behaviors and cannot be used together on the same attribute."); + }); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java index dd059c9a271c..0720ffa99d8c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java @@ -34,7 +34,7 @@ public class AutoGeneratedKeyExtensionTest { private static final OperationContext PRIMARY_CONTEXT = DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName()); - private final AutoGeneratedKeyExtension extension = AutoGeneratedKeyExtension.builder().build(); + private final AutoGeneratedKeyExtension extension = AutoGeneratedKeyExtension.create(); /** * Schema that places @DynamoDbAutoGeneratedKey on GSI key ("keyAttribute") -> the validation passes. @@ -250,9 +250,9 @@ public void autoGeneratedKey_onNonKeyAttribute_throwsIllegalArgumentException() .operationContext(PRIMARY_CONTEXT) .build()) ) - .withMessageContaining("@DynamoDbAutoGeneratedKey can only be applied to key attributes: " - + "primary partition key, primary sort key, or GSI/LSI partition/sort keys.") - .withMessageContaining("keyAttribute"); + .withMessage("@DynamoDbAutoGeneratedKey can only be applied to key attributes: " + + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. " + + "Invalid placement on attribute: keyAttribute"); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutogeneratedConflictingAnnotationsTest.java similarity index 99% rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutogeneratedConflictingAnnotationsTest.java index 456650ec706f..933bc9f64f86 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ConflictingAnnotationsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutogeneratedConflictingAnnotationsTest.java @@ -34,14 +34,14 @@ * Tests to verify that conflicting annotations (@DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid) are properly detected * and throw exceptions regardless of extension load order. */ -public class ConflictingAnnotationsTest { +public class AutogeneratedConflictingAnnotationsTest { private static final String RECORD_ID = "1"; private static final String TABLE_NAME = "table-name"; private static final OperationContext PRIMARY_CONTEXT = DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName()); - private final AutoGeneratedKeyExtension keyExtension = AutoGeneratedKeyExtension.builder().build(); + private final AutoGeneratedKeyExtension keyExtension = AutoGeneratedKeyExtension.create(); private final AutoGeneratedUuidExtension uuidExtension = AutoGeneratedUuidExtension.create(); /** diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtilsTest.java new file mode 100644 index 000000000000..bc78b5b45f06 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtilsTest.java @@ -0,0 +1,92 @@ +package software.amazon.awssdk.enhanced.dynamodb.extensions; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.ExtensionsValidationUtils.validateNoAnnotationConflict; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; + +@RunWith(MockitoJUnitRunner.class) +public class ExtensionsValidationUtilsTest { + + @Mock + private TableMetadata metadata; + + private static final String AUTOGENERATED_KEY_METADATA_KEY = + "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute"; + private static final String AUTOGENERATED_UUID_METADATA_KEY = + "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; + + private static final String AUTOGENERATED_KEY_ANNOTATION = "@DynamoDbAutoGeneratedKey"; + private static final String AUTOGENERATED_UUID_ANNOTATION = "@DynamoDbAutoGeneratedUuid"; + + @Test + public void validateNoAnnotationConflict_whenAnnotationsOverlap_throwsException() { + when(metadata.customMetadataObject(AUTOGENERATED_KEY_METADATA_KEY, Collection.class)) + .thenReturn(Optional.of(Collections.singleton("sharedAttribute"))); + when(metadata.customMetadataObject(AUTOGENERATED_UUID_METADATA_KEY, Collection.class)) + .thenReturn(Optional.of(Arrays.asList("sharedAttribute", "otherUuidAttribute"))); + + assertThatThrownBy(() -> validateNoAnnotationConflict( + metadata, + AUTOGENERATED_KEY_METADATA_KEY, + AUTOGENERATED_UUID_METADATA_KEY, + AUTOGENERATED_KEY_ANNOTATION, + AUTOGENERATED_UUID_ANNOTATION + )) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Attribute 'sharedAttribute' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid " + + "annotations. These annotations have conflicting behaviors and cannot be used together on the same attribute."); + } + + @Test + public void validateNoAnnotationConflict_whenNoAnnotatedFields_doesNotThrowException() { + assertThatCode(() -> validateNoAnnotationConflict( + metadata, + AUTOGENERATED_KEY_METADATA_KEY, + AUTOGENERATED_UUID_METADATA_KEY, + AUTOGENERATED_KEY_ANNOTATION, + AUTOGENERATED_UUID_ANNOTATION + )).doesNotThrowAnyException(); + } + + @Test + public void validateNoAnnotationConflict_whenAnnotationsDontOverlap_doesNotThrowException() { + when(metadata.customMetadataObject(AUTOGENERATED_KEY_METADATA_KEY, Collection.class)) + .thenReturn(Optional.of(Collections.singleton("keyAttribute"))); + when(metadata.customMetadataObject(AUTOGENERATED_UUID_METADATA_KEY, Collection.class)) + .thenReturn(Optional.of(Collections.singleton("uuidAttribute"))); + + assertThatCode(() -> validateNoAnnotationConflict( + metadata, + AUTOGENERATED_KEY_METADATA_KEY, + AUTOGENERATED_UUID_METADATA_KEY, + AUTOGENERATED_KEY_ANNOTATION, + AUTOGENERATED_UUID_ANNOTATION + )).doesNotThrowAnyException(); + } + + @Test + public void validateNoAnnotationConflict_whenSecondAnnotationIsNotUsed_doesNotThrow() { + when(metadata.customMetadataObject(AUTOGENERATED_KEY_METADATA_KEY, Collection.class)) + .thenReturn(Optional.empty()); + + assertThatCode(() -> validateNoAnnotationConflict( + metadata, + AUTOGENERATED_KEY_METADATA_KEY, + AUTOGENERATED_UUID_METADATA_KEY, + AUTOGENERATED_KEY_ANNOTATION, + AUTOGENERATED_UUID_ANNOTATION + )).doesNotThrowAnyException(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java index f2b3283b0650..2902465297f2 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java @@ -60,7 +60,7 @@ public class AutoGeneratedKeyRecordTest extends LocalDynamoDbSyncTestBase { public AutoGeneratedKeyRecordTest(String testName, TableSchema schema) { this.mappedTable = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedKeyExtension.builder().build()) + .extensions(AutoGeneratedKeyExtension.create()) .build() .table(getConcreteTableName("AutoGenKey-table"), schema); } @@ -188,7 +188,7 @@ public void putItem_withAutogeneratedKeySetOnNonKeyAttribute_throwsException() { .isThrownBy(() -> table.putItem(record)) .withMessage( "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " - + "primary partition key, primary sort key, or GSI/LSI partition/sort keys." + + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. " + "Invalid placement on attribute: notAKey"); } finally { deleteTableByName(tableName); @@ -201,8 +201,9 @@ public void versionedRecord_worksWithAutoGeneratedKeys() { String tableName = getConcreteTableName("versioned-test"); DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedKeyExtension.builder().build(), - VersionedRecordExtension.builder().build()) + .extensions( + AutoGeneratedKeyExtension.builder().build(), + VersionedRecordExtension.builder().build()) .build(); DynamoDbTable table = client.table(tableName, TableSchema.fromBean(VersionedRecord.class)); @@ -236,8 +237,9 @@ public void autogeneratedConflictingAnnotations_throwsException() { String tableName = getConcreteTableName("conflicting-annotations-test"); DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedKeyExtension.builder().build(), - AutoGeneratedUuidExtension.create()) + .extensions( + AutoGeneratedKeyExtension.create(), + AutoGeneratedUuidExtension.create()) .build(); try { @@ -376,7 +378,7 @@ public void batchWrite_mixedPutDeleteOperations() { private DynamoDbEnhancedClient createClient() { return DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedKeyExtension.builder().build()) + .extensions(AutoGeneratedKeyExtension.create()) .build(); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java index 161cf7b30fd5..71f79bf7851e 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedUuidRecordTest.java @@ -318,7 +318,7 @@ public void putItem_onRecordWithAutogeneratedConflictingAnnotations_throwsExcept DynamoDbEnhancedClient client = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedUuidExtension.create(), AutoGeneratedKeyExtension.builder().build()) + .extensions(AutoGeneratedUuidExtension.create(), AutoGeneratedKeyExtension.create()) .build(); try { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java index 2e1ca89a0830..de98bb27c519 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java @@ -54,7 +54,7 @@ public class AutoGeneratedKeyExtensionTest extends LocalDynamoDbSyncTestBase { .dynamoDbClient(getDynamoDbClient()) .extensions(Stream.concat( ExtensionResolver.defaultExtensions().stream(), - Stream.of(AutoGeneratedKeyExtension.builder().build())) + Stream.of(AutoGeneratedKeyExtension.create())) .collect(Collectors.toList())) .build(); From a4bfb1e6134f0246447b64da5e249cbb6ad00af4 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 13 Feb 2026 12:11:20 +0200 Subject: [PATCH 09/14] Addressed PR feedback --- .../AutoGeneratedUuidExtension.java | 6 ++- .../extensions/ExtensionsValidationUtils.java | 24 +++++----- .../annotations/DynamoDbAutoGeneratedKey.java | 47 ++++--------------- .../DynamoDbAutoGeneratedUuid.java | 18 +++++-- 4 files changed, 41 insertions(+), 54 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java index e3f7b8922d07..9c8e55d82a10 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java @@ -101,6 +101,8 @@ public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientE "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute"; private static final AutoGeneratedUuidAttribute AUTO_GENERATED_UUID_ATTRIBUTE = new AutoGeneratedUuidAttribute(); + private static final String AUTOGENERATED_KEY_ANNOTATION = "@DynamoDbAutoGeneratedKey"; + private static final String AUTOGENERATED_UUID_ANNOTATION = "@DynamoDbAutoGeneratedUuid"; /** * Stores the TableMetadata instances that have already been validated by this extension. Uses a ConcurrentHashMap to ensure @@ -156,8 +158,8 @@ private void validateNoAnnotationConflict(TableMetadata tableMetadata) { tableMetadata, AUTOGENERATED_KEY_EXTENSION_METADATA_KEY, CUSTOM_METADATA_KEY, - "@DynamoDbAutoGeneratedKey", - "@DynamoDbAutoGeneratedUuid"); + AUTOGENERATED_KEY_ANNOTATION, + AUTOGENERATED_UUID_ANNOTATION); } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java index 1b7a81e2ed6a..4a04286e2ece 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/ExtensionsValidationUtils.java @@ -35,34 +35,34 @@ private ExtensionsValidationUtils() { * thrown with a message indicating the attribute and the conflicting annotations. * * @param tableMetadata The metadata of the table to validate. - * @param firstMetadataKey The metadata key for the first annotation to check for. - * @param secondMetadataKey The metadata key for the second annotation to check for. + * @param firstAnnotationMetadataKey The metadata key for the first annotation to check for. + * @param secondAnnotationMetadataKey The metadata key for the second annotation to check for. * @param firstAnnotationName The name of the first annotation to use in the error message if a conflict is found. * @param secondAnnotationName The name of the second annotation to use in the error message if a conflict is found. */ public static void validateNoAnnotationConflict(TableMetadata tableMetadata, - String firstMetadataKey, - String secondMetadataKey, + String firstAnnotationMetadataKey, + String secondAnnotationMetadataKey, String firstAnnotationName, String secondAnnotationName) { - Collection attributesWithFirstAnnotation = - tableMetadata.customMetadataObject(firstMetadataKey, Collection.class).orElse(Collections.emptyList()); + Collection attributesHavingFirstAnnotation = + tableMetadata.customMetadataObject(firstAnnotationMetadataKey, Collection.class).orElse(Collections.emptyList()); - if (attributesWithFirstAnnotation.isEmpty()) { + if (attributesHavingFirstAnnotation.isEmpty()) { return; } - Collection attributesWithSecondAnnotation = - tableMetadata.customMetadataObject(secondMetadataKey, Collection.class).orElse(Collections.emptyList()); + Collection attributesHavingSecondAnnotation = + tableMetadata.customMetadataObject(secondAnnotationMetadataKey, Collection.class).orElse(Collections.emptyList()); - if (attributesWithSecondAnnotation.isEmpty()) { + if (attributesHavingSecondAnnotation.isEmpty()) { return; } - attributesWithFirstAnnotation + attributesHavingFirstAnnotation .stream() - .filter(attributesWithSecondAnnotation::contains) + .filter(attributesHavingSecondAnnotation::contains) .findFirst() .ifPresent(attribute -> { throw new IllegalArgumentException( diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java index 6fbb90d5a0c8..cdad8d30616b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java @@ -27,47 +27,20 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; /** - * Annotation that marks a key attribute to be automatically populated with a random UUID if no value is provided during a write - * operation (put or update). This annotation is intended to work specifically with key attributes. + * Marks a key attribute to be automatically populated with a UUID when the value is null or empty during a write operation. * - *

This annotation is designed for use with the V2 {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}. - * It is registered via {@link BeanTableSchemaAttributeTag} and its behavior is implemented by - * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension}.

+ *

Usage: May only be applied to key attributes: primary partition key, primary sort key, or secondary index + * (GSI/LSI) keys. If used on a non-key attribute, {@code AutoGeneratedKeyExtension} throws an {@link IllegalArgumentException} + * during schema validation. * - *

Where this annotation can be applied

- * This annotation is only valid on attributes that serve as keys: - *
    - *
  • The table's primary partition key or sort key
  • - *
  • The partition key or sort key of a secondary index (GSI or LSI)
  • - *
- * If applied to any other attribute, the {@code AutoGeneratedKeyExtension} will throw an - * {@link IllegalArgumentException} at runtime. + *

Semantics: Generates a UUID using {@link java.util.UUID#randomUUID()} + * only when the attribute is absent. Existing values are preserved. * - *

How values are generated

- *
    - *
  • On writes where the annotated attribute is null or empty, a new UUID value is generated - * using {@link java.util.UUID#randomUUID()}.
  • - *
  • If a value is already set on the attribute, that value is preserved and not replaced.
  • - *
  • This behavior differs from {@code @DynamoDbAutoGeneratedUuid}, which always generates new UUIDs regardless of existing - * values.
  • - *
+ *

Difference from {@code @DynamoDbAutoGeneratedUuid}: + * This annotation is intended for key attributes and generates a value only when missing. {@code @DynamoDbAutoGeneratedUuid} can + * be applied to any attribute and always generates a new UUID on every write. * - *

Behavior with UpdateBehavior

- *

Primary Keys: {@link DynamoDbUpdateBehavior} has no effect on primary partition keys - * or primary sort keys. Primary keys are immutable in DynamoDB and cannot use conditional update behaviors like - * {@link UpdateBehavior#WRITE_IF_NOT_EXISTS}. UUIDs will be generated whenever the primary key attribute is missing - * or empty, regardless of any {@code UpdateBehavior} setting.

- * - *

Secondary Index Keys: For GSI/LSI keys, {@link DynamoDbUpdateBehavior} can be used: - *

    - *
  • {@link UpdateBehavior#WRITE_ALWAYS} (default) – Generate a new UUID whenever the attribute is missing during write.
  • - *
  • {@link UpdateBehavior#WRITE_IF_NOT_EXISTS} – Generate a UUID only on the first write, preserving the value on - * subsequent updates.
  • - *
- *

- * - *

Type restriction

- * This annotation is only valid on attributes of type {@link String}. + *

Valid only for {@link String} attributes. */ @SdkPublicApi @Documented diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedUuid.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedUuid.java index 6df85903c20a..5381cd77227b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedUuid.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedUuid.java @@ -19,14 +19,26 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.UUID; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AutoGeneratedUuidTag; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag; /** - * Denotes this attribute as recording the auto generated UUID string for the record. Every time a record with this - * attribute is written to the database it will update the attribute with a {@link UUID#randomUUID} string. + * Marks an attribute to be automatically populated with a new UUID on every write operation. + * + *

Intended Usage: + * This annotation is generic and may be applied to any {@link String} attribute, not only key attributes. + * + *

Generation Semantics:

+ * On every write (put, update, batch write, or transaction write), the attribute is replaced with a new value generated using + * {@link java.util.UUID#randomUUID()}, regardless of any existing value. + * + *

Difference from {@code @DynamoDbAutoGeneratedKey}: + * {@code @DynamoDbAutoGeneratedKey} is intended for key attributes and generates UUID only when the value is absent. + * {@code @DynamoDbAutoGeneratedUuid} annotation always regenerates the UUID on each write.

+ * + *

Type Restriction:

+ * This annotation may only be applied to attributes of type {@link String}. */ @SdkPublicApi @Target(ElementType.METHOD) From bbd03ac31eaa2226330be7a63ebdffa92cb689f8 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Mon, 16 Feb 2026 16:28:08 +0200 Subject: [PATCH 10/14] Added tests with composite gsi --- .../extensions/AutoGeneratedKeyExtension.java | 23 +- .../AutoGeneratedKeyExtensionTest.java | 663 +++++++++++++++--- 2 files changed, 582 insertions(+), 104 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java index 43a1943b521a..e06bce6cb643 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java @@ -27,13 +27,14 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; -import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; @@ -164,20 +165,12 @@ private static void validateNoAutoGeneratedAnnotationConflict(TableMetadata tabl private void validateAutoGeneratedKeyPlacement(TableMetadata tableMetadata, Collection taggedAttributeNames) { - Set allowedKeyAttributes = allowedKeysCache.computeIfAbsent(tableMetadata, metadata -> { - Set keyAttributes = new HashSet<>(); - keyAttributes.add(metadata.primaryPartitionKey()); - metadata.primarySortKey().ifPresent(keyAttributes::add); - - metadata.indices().stream() - .map(IndexMetadata::name) - .forEach(indexName -> { - keyAttributes.add(metadata.indexPartitionKey(indexName)); - metadata.indexSortKey(indexName).ifPresent(keyAttributes::add); - }); - - return keyAttributes; - }); + Set allowedKeyAttributes = + allowedKeysCache.computeIfAbsent(tableMetadata, metadata -> + new HashSet<>(metadata.keyAttributes() + .stream() + .map(KeyAttributeMetadata::name) + .collect(Collectors.toSet()))); taggedAttributeNames.stream() .filter(attrName -> !allowedKeyAttributes.contains(attrName)) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java index de98bb27c519..514bcea5d225 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java @@ -18,6 +18,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.FIRST; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.SECOND; import java.util.List; import java.util.stream.Collectors; @@ -34,8 +36,10 @@ import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; @@ -46,8 +50,8 @@ public class AutoGeneratedKeyExtensionTest extends LocalDynamoDbSyncTestBase { - private static final TableSchema TABLE_SCHEMA = - TableSchema.fromClass(AutogeneratedKeyRecord.class); + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(BeanWithAutogeneratedKey.class); private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() @@ -58,7 +62,7 @@ public class AutoGeneratedKeyExtensionTest extends LocalDynamoDbSyncTestBase { .collect(Collectors.toList())) .build(); - private final DynamoDbTable mappedTable = + private final DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName("autogenerated-key-table"), TABLE_SCHEMA); @Before @@ -73,11 +77,11 @@ public void deleteTable() { @Test public void putItem_whenKeysNotPopulated_generatesNewUuids() { - AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); + BeanWithAutogeneratedKey record = new BeanWithAutogeneratedKey(); mappedTable.putItem(record); - AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); + BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); isValidUuid(result.getId()); isValidUuid(result.getSortKey()); @@ -87,18 +91,14 @@ public void putItem_whenKeysNotPopulated_generatesNewUuids() { @Test public void putItem_whenKeysAlreadyPopulated_preservesExistingUuids() { - AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); - record.setId("existing-id"); - record.setSortKey("existing-sk"); - record.setGsiPk("existing-gsiPk"); - record.setGsiSk("existing-gsiSk"); + BeanWithAutogeneratedKey record = buildBeanWithAutogeneratedKeyAndKeysPopulated(); mappedTable.putItem(record); - AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); + BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); assertThat(result.getId()).isEqualTo("existing-id"); - assertThat(result.getSortKey()).isEqualTo("existing-sk"); + assertThat(result.getSortKey()).isEqualTo("existing-sortKey"); assertThat(result.getGsiPk()).isEqualTo("existing-gsiPk"); assertThat(result.getGsiSk()).isEqualTo("existing-gsiSk"); } @@ -106,10 +106,10 @@ public void putItem_whenKeysAlreadyPopulated_preservesExistingUuids() { @Test public void updateItem_respectsUpdateBehavior() { // put initial item - AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); + BeanWithAutogeneratedKey record = new BeanWithAutogeneratedKey(); mappedTable.putItem(record); - AutogeneratedKeyRecord afterPut = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); + BeanWithAutogeneratedKey afterPut = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); String originalPk = afterPut.getId(); String originalSk = afterPut.getSortKey(); String originalGsiPk = afterPut.getGsiPk(); @@ -117,38 +117,38 @@ public void updateItem_respectsUpdateBehavior() { // update item - AutogeneratedKeyRecord update = new AutogeneratedKeyRecord(); + BeanWithAutogeneratedKey update = new BeanWithAutogeneratedKey(); update.setId(afterPut.getId()); update.setSortKey(afterPut.getSortKey()); mappedTable.updateItem(update); - AutogeneratedKeyRecord afterUpdate = + BeanWithAutogeneratedKey afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSortKey()))); // id and sortKey preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys assertThat(afterUpdate.getId()).isEqualTo(originalPk); assertThat(afterUpdate.getSortKey()).isEqualTo(originalSk); - // gsiPk has WRITE_ALWAYS: regenerates UUID on every update + // gsiPk has WRITE_ALWAYS -> regenerates UUID on every update isValidUuid(afterUpdate.getGsiPk()); assertThat(afterUpdate.getGsiPk()).isNotEqualTo(originalGsiPk); - // gsiSk has WRITE_IF_NOT_EXISTS: preserves original UUID, only writes if null + // gsiSk has WRITE_IF_NOT_EXISTS -> preserves original UUID, only writes if null assertThat(afterUpdate.getGsiSk()).isEqualTo(originalGsiSk); } @Test public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { - AutogeneratedKeyRecord firstRecord = new AutogeneratedKeyRecord(); - AutogeneratedKeyRecord secondRecord = new AutogeneratedKeyRecord(); + BeanWithAutogeneratedKey firstRecord = new BeanWithAutogeneratedKey(); + BeanWithAutogeneratedKey secondRecord = new BeanWithAutogeneratedKey(); enhancedClient.batchWriteItem(req -> req.addWriteBatch( - WriteBatch.builder(AutogeneratedKeyRecord.class) + WriteBatch.builder(BeanWithAutogeneratedKey.class) .mappedTableResource(mappedTable) .addPutItem(firstRecord) .addPutItem(secondRecord) .build())); - List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); assertThat(results.size()).isEqualTo(2); isValidUuid(results.get(0).getId()); @@ -159,51 +159,42 @@ public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { @Test public void batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { - AutogeneratedKeyRecord firstRecord = new AutogeneratedKeyRecord(); - firstRecord.setId("existing-id-1"); - firstRecord.setSortKey("existing-sk-1"); - firstRecord.setGsiPk("existing-gsiPk-1"); - firstRecord.setGsiSk("existing-gsiSk-1"); - - AutogeneratedKeyRecord secondRecord = new AutogeneratedKeyRecord(); - secondRecord.setId("existing-id-2"); - secondRecord.setSortKey("existing-sk-2"); - secondRecord.setGsiPk("existing-gsiPk-2"); - secondRecord.setGsiSk("existing-gsiSk-2"); + BeanWithAutogeneratedKey firstRecord = buildBeanWithAutogeneratedKeyAndKeysPopulated(1); + BeanWithAutogeneratedKey secondRecord = buildBeanWithAutogeneratedKeyAndKeysPopulated(2); enhancedClient.batchWriteItem(req -> req.addWriteBatch( - WriteBatch.builder(AutogeneratedKeyRecord.class) + WriteBatch.builder(BeanWithAutogeneratedKey.class) .mappedTableResource(mappedTable) .addPutItem(firstRecord) .addPutItem(secondRecord) .build())); - AutogeneratedKeyRecord savedRecord1 = - mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id-1").sortValue("existing-sk-1"))); - assertThat(savedRecord1.getId()).isEqualTo("existing-id-1"); - assertThat(savedRecord1.getSortKey()).isEqualTo("existing-sk-1"); - assertThat(savedRecord1.getGsiPk()).isEqualTo("existing-gsiPk-1"); - assertThat(savedRecord1.getGsiSk()).isEqualTo("existing-gsiSk-1"); - - AutogeneratedKeyRecord savedRecord2 = - mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id-2").sortValue("existing-sk-2"))); - assertThat(savedRecord2.getId()).isEqualTo("existing-id-2"); - assertThat(savedRecord2.getSortKey()).isEqualTo("existing-sk-2"); - assertThat(savedRecord2.getGsiPk()).isEqualTo("existing-gsiPk-2"); - assertThat(savedRecord2.getGsiSk()).isEqualTo("existing-gsiSk-2"); + BeanWithAutogeneratedKey savedRecord1 = + mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_1").sortValue("existing-sortKey_1"))); + assertThat(savedRecord1.getId()).isEqualTo("existing-id_1"); + assertThat(savedRecord1.getSortKey()).isEqualTo("existing-sortKey_1"); + assertThat(savedRecord1.getGsiPk()).isEqualTo("existing-gsiPk_1"); + assertThat(savedRecord1.getGsiSk()).isEqualTo("existing-gsiSk_1"); + + BeanWithAutogeneratedKey savedRecord2 = + mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_2").sortValue("existing-sortKey_2"))); + assertThat(savedRecord2.getId()).isEqualTo("existing-id_2"); + assertThat(savedRecord2.getSortKey()).isEqualTo("existing-sortKey_2"); + assertThat(savedRecord2.getGsiPk()).isEqualTo("existing-gsiPk_2"); + assertThat(savedRecord2.getGsiSk()).isEqualTo("existing-gsiSk_2"); } @Test public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { - AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); + BeanWithAutogeneratedKey record = new BeanWithAutogeneratedKey(); enhancedClient.transactWriteItems( TransactWriteItemsEnhancedRequest.builder() .addPutItem(mappedTable, record) .build()); - AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); + BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); isValidUuid(result.getId()); isValidUuid(result.getSortKey()); @@ -213,48 +204,44 @@ public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { @Test public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { - AutogeneratedKeyRecord record = new AutogeneratedKeyRecord(); - record.setId("existing-id-1"); - record.setSortKey("existing-sk-1"); - record.setGsiPk("existing-gsiPk-1"); - record.setGsiSk("existing-gsiSk-1"); + BeanWithAutogeneratedKey record = buildBeanWithAutogeneratedKeyAndKeysPopulated(); enhancedClient.transactWriteItems( TransactWriteItemsEnhancedRequest.builder() .addPutItem(mappedTable, record) .build()); - AutogeneratedKeyRecord result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); + BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); - assertThat(result.getId()).isEqualTo("existing-id-1"); - assertThat(result.getSortKey()).isEqualTo("existing-sk-1"); - assertThat(result.getGsiPk()).isEqualTo("existing-gsiPk-1"); - assertThat(result.getGsiSk()).isEqualTo("existing-gsiSk-1"); + assertThat(result.getId()).isEqualTo("existing-id"); + assertThat(result.getSortKey()).isEqualTo("existing-sortKey"); + assertThat(result.getGsiPk()).isEqualTo("existing-gsiPk"); + assertThat(result.getGsiSk()).isEqualTo("existing-gsiSk"); } @Test public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() { String tableName = "versioned-record-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyVersionedRecord.class)); + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithAutogeneratedKeyAndVersion.class)); try { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - AutogeneratedKeyVersionedRecord record = new AutogeneratedKeyVersionedRecord(); + BeanWithAutogeneratedKeyAndVersion record = new BeanWithAutogeneratedKeyAndVersion(); record.setId("id"); record.setData("data-v1"); mappedTable.putItem(record); - AutogeneratedKeyVersionedRecord retrieved = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); + BeanWithAutogeneratedKeyAndVersion retrieved = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); isValidUuid(retrieved.getId()); assertThat(retrieved.getData()).isEqualTo("data-v1"); assertThat(retrieved.getVersion()).isEqualTo(1L); retrieved.setData("data-v2"); - AutogeneratedKeyVersionedRecord updated = mappedTable.updateItem(retrieved); + BeanWithAutogeneratedKeyAndVersion updated = mappedTable.updateItem(retrieved); isValidUuid(updated.getId()); assertThat(updated.getData()).isEqualTo("data-v2"); assertThat(updated.getVersion()).isEqualTo(2L); @@ -270,13 +257,13 @@ public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() { @Test public void putItem_whenAnnotationInConflictWithAutogeneratedUuidAnnotation_throwsException() { String tableName = "conflicting-annotations-record-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyConflictingRecord.class)); + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithConflictingAnnotations.class)); try { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyConflictingRecord())) + assertThatThrownBy(() -> mappedTable.putItem(new BeanWithConflictingAnnotations())) .isInstanceOf(IllegalArgumentException.class) .hasMessage( "Attribute 'id' cannot have both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid annotations. " @@ -292,13 +279,13 @@ public void putItem_whenAnnotationInConflictWithAutogeneratedUuidAnnotation_thro @Test public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() { String tableName = "annotation-on-non-key-attribute-record-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(AutogeneratedKeyOnNonKeyAttributeRecord.class)); + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithAutogeneratedKeyOnNonKeyAttribute.class)); try { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - assertThatThrownBy(() -> mappedTable.putItem(new AutogeneratedKeyOnNonKeyAttributeRecord())) + assertThatThrownBy(() -> mappedTable.putItem(new BeanWithAutogeneratedKeyOnNonKeyAttribute())) .isInstanceOf(IllegalArgumentException.class) .hasMessage( "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " @@ -315,23 +302,24 @@ public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() { @Test public void putItem_whenNoAutogeneratedKeyAnnotationIsPresent_doesNotRegenerateUuids() { String tableName = "no-annotation-record-autogenerated-key-table"; - DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(BeanRecord.class)); + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithoutAutogeneratedKey.class)); try { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - BeanRecord record = new BeanRecord(); + BeanWithoutAutogeneratedKey record = new BeanWithoutAutogeneratedKey(); record.setId("existing-id"); - record.setSortKey("existing-sk"); + record.setSortKey("existing-sortKey"); record.setGsiPk("existing-gsiPk"); record.setGsiSk("existing-gsiSk"); record.setData("test"); mappedTable.putItem(record); - BeanRecord retrieved = mappedTable.getItem( - r -> r.key(k -> k.partitionValue("existing-id").sortValue("existing-sk"))); + BeanWithoutAutogeneratedKey retrieved = mappedTable.getItem( + r -> r.key(k -> k.partitionValue("existing-id").sortValue("existing-sortKey"))); assertThat(retrieved.getId()).isEqualTo("existing-id"); - assertThat(retrieved.getSortKey()).isEqualTo("existing-sk"); + assertThat(retrieved.getSortKey()).isEqualTo("existing-sortKey"); assertThat(retrieved.getGsiPk()).isEqualTo("existing-gsiPk"); assertThat(retrieved.getGsiSk()).isEqualTo("existing-gsiSk"); assertThat(retrieved.getData()).isEqualTo("test"); @@ -345,15 +333,376 @@ public void putItem_whenNoAutogeneratedKeyAnnotationIsPresent_doesNotRegenerateU @Test public void createBean_givenAutogeneratedKeyAnnotationAppliedOnNonStringAttributeType_throwsException() { - assertThatThrownBy(() -> TableSchema.fromBean(AutogeneratedKeyInvalidTypeRecord.class)) + assertThatThrownBy(() -> TableSchema.fromBean(BeanWithAutogeneratedKeyOnAttributeWithInvalidType.class)) .isInstanceOf(IllegalArgumentException.class) .hasMessage( "Attribute 'id' of Class type class java.lang.Integer is not a suitable Java Class type to be used " + "as a Auto Generated Key attribute. Only String Class type is supported."); } + + // Tests with Mixed Composite Gsi + @Test + public void putItem_onBeanWithCompositeGsi_whenKeysNotPopulated_generatesNewUuids() { + String tableName = "mixed-gsi-autogenerated-key-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); + mappedTable.putItem(record); + + BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream() + .findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + isValidUuid(result.getId()); + isValidUuid(result.getSort()); + isValidUuid(result.getRootPartitionKey1()); + isValidUuid(result.getRootPartitionKey2()); + isValidUuid(result.getRootSortKey1()); + isValidUuid(result.getRootSortKey2()); + isValidUuid(result.getFlattenedKeys().flattenedPartitionKey1); + isValidUuid(result.getFlattenedKeys().flattenedPartitionKey2); + isValidUuid(result.getFlattenedKeys().flattenedSortKey1); + isValidUuid(result.getFlattenedKeys().flattenedSortKey2); + + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void putItem_onBeanWithCompositeGsi_whenKeysAlreadyPopulated_preservesExistingUuids() { + String tableName = "mixed-gsi-autogenerated-key-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); + mappedTable.putItem(record); + + BeanWithMixedCompositeGsi result = + mappedTable.scan().items().stream() + .findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + + assertThat(result.getId()).isEqualTo("existing-id"); + assertThat(result.getSort()).isEqualTo("existing-sort"); + assertThat(result.getRootPartitionKey1()).isEqualTo("existing-rootPk1"); + assertThat(result.getRootPartitionKey2()).isEqualTo("existing-rootPk2"); + assertThat(result.getRootSortKey1()).isEqualTo("existing-rootSk1"); + assertThat(result.getRootSortKey2()).isEqualTo("existing-rootSk2"); + assertThat(result.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1"); + assertThat(result.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2"); + assertThat(result.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1"); + assertThat(result.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2"); + + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void updateItem_onBeanWithCompositeKeys_respectsUpdateBehavior() { + String tableName = "mixed-gsi-autogenerated-key-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); + mappedTable.putItem(record); + + // put initial item + BeanWithMixedCompositeGsi afterPut = + mappedTable.scan().items().stream() + .findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + String originalPk = afterPut.getId(); + String originalSk = afterPut.getSort(); + String originalRootPartitionKey1 = afterPut.getRootPartitionKey1(); + String originalRootPartitionKey2 = afterPut.getRootPartitionKey2(); + String originalRootSortKey1 = afterPut.getRootSortKey1(); + String originalRootSortKey2 = afterPut.getRootSortKey2(); + String originalFlattenedPartitionKey1 = afterPut.getFlattenedKeys().flattenedPartitionKey1; + String originalFlattenedPartitionKey2 = afterPut.getFlattenedKeys().flattenedPartitionKey2; + String originalFlattenedSortKey1 = afterPut.getFlattenedKeys().flattenedSortKey1; + String originalFlattenedSortKey2 = afterPut.getFlattenedKeys().flattenedSortKey2; + + // update item + BeanWithMixedCompositeGsi update = new BeanWithMixedCompositeGsi(); + update.setId(afterPut.getId()); + update.setSort(afterPut.getSort()); + + mappedTable.updateItem(update); + BeanWithMixedCompositeGsi afterUpdate = + mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSort()))); + + + // id and sort preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys + assertThat(afterUpdate.getId()).isEqualTo(originalPk); + assertThat(afterUpdate.getSort()).isEqualTo(originalSk); + + // rootPartitionKey1, rootSortKey1, flattenedPartitionKey1, flattenedSortKey1 have WRITE_ALWAYS + // -> regenerates UUID on every update + isValidUuid(afterUpdate.getRootPartitionKey1()); + isValidUuid(afterUpdate.getRootSortKey1()); + assertThat(afterUpdate.getRootPartitionKey1()).isNotEqualTo(originalRootPartitionKey1); + assertThat(afterUpdate.getRootSortKey1()).isNotEqualTo(originalRootSortKey1); + + isValidUuid(afterUpdate.getFlattenedKeys().flattenedPartitionKey1); + isValidUuid(afterUpdate.getFlattenedKeys().flattenedSortKey1); + assertThat(afterUpdate.getFlattenedKeys().flattenedPartitionKey1).isNotEqualTo(originalFlattenedPartitionKey1); + assertThat(afterUpdate.getFlattenedKeys().flattenedSortKey1).isNotEqualTo(originalFlattenedSortKey1); + + + // rootPartitionKey2, rootSortKey2, flattenedPartitionKey2, flattenedSortKey2 have WRITE_IF_NOT_EXISTS + // -> preserves original UUID, only writes if null + assertThat(afterUpdate.getRootPartitionKey2()).isEqualTo(originalRootPartitionKey2); + assertThat(afterUpdate.getRootSortKey2()).isEqualTo(originalRootSortKey2); + assertThat(afterUpdate.getFlattenedKeys().getFlattenedPartitionKey2()).isEqualTo(originalFlattenedPartitionKey2); + assertThat(afterUpdate.getFlattenedKeys().getFlattenedSortKey2()).isEqualTo(originalFlattenedSortKey2); + + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() { + String tableName = "mixed-gsi-autogenerated-key-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1); + BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(BeanWithMixedCompositeGsi.class) + .mappedTableResource(mappedTable) + .addPutItem(firstRecord) + .addPutItem(secondRecord) + .build())); + + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + + assertThat(results.size()).isEqualTo(2); + isValidUuid(results.get(0).getId()); + isValidUuid(results.get(1).getId()); + isValidUuid(results.get(0).getSort()); + isValidUuid(results.get(1).getSort()); + isValidUuid(results.get(0).getRootPartitionKey1()); + isValidUuid(results.get(1).getRootPartitionKey1()); + isValidUuid(results.get(0).getRootPartitionKey2()); + isValidUuid(results.get(1).getRootPartitionKey2()); + isValidUuid(results.get(0).getRootSortKey1()); + isValidUuid(results.get(1).getRootSortKey1()); + isValidUuid(results.get(0).getRootSortKey2()); + isValidUuid(results.get(1).getRootSortKey2()); + isValidUuid(results.get(0).getFlattenedKeys().flattenedPartitionKey1); + isValidUuid(results.get(1).getFlattenedKeys().flattenedPartitionKey1); + isValidUuid(results.get(0).getFlattenedKeys().flattenedPartitionKey2); + isValidUuid(results.get(1).getFlattenedKeys().flattenedPartitionKey2); + isValidUuid(results.get(0).getFlattenedKeys().flattenedSortKey1); + isValidUuid(results.get(1).getFlattenedKeys().flattenedSortKey1); + isValidUuid(results.get(0).getFlattenedKeys().flattenedSortKey2); + isValidUuid(results.get(1).getFlattenedKeys().flattenedSortKey2); + + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() { + String tableName = "mixed-gsi-autogenerated-key-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1); + BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch(WriteBatch.builder(BeanWithMixedCompositeGsi.class) + .mappedTableResource(mappedTable) + .addPutItem(firstRecord) + .addPutItem(secondRecord) + .build())); + + BeanWithMixedCompositeGsi firstSavedRecord = + mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_1").sortValue("existing-sort_1"))); + assertThat(firstSavedRecord.getId()).isEqualTo("existing-id_1"); + assertThat(firstSavedRecord.getSort()).isEqualTo("existing-sort_1"); + assertThat(firstSavedRecord.getRootPartitionKey1()).isEqualTo("existing-rootPk1_1"); + assertThat(firstSavedRecord.getRootPartitionKey2()).isEqualTo("existing-rootPk2_1"); + assertThat(firstSavedRecord.getRootSortKey1()).isEqualTo("existing-rootSk1_1"); + assertThat(firstSavedRecord.getRootSortKey2()).isEqualTo("existing-rootSk2_1"); + assertThat(firstSavedRecord.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_1"); + assertThat(firstSavedRecord.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_1"); + assertThat(firstSavedRecord.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_1"); + assertThat(firstSavedRecord.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_1"); + + + BeanWithMixedCompositeGsi secondSavedRecord = + mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_2").sortValue("existing-sort_2"))); + + assertThat(secondSavedRecord.getId()).isEqualTo("existing-id_2"); + assertThat(secondSavedRecord.getSort()).isEqualTo("existing-sort_2"); + assertThat(secondSavedRecord.getRootPartitionKey1()).isEqualTo("existing-rootPk1_2"); + assertThat(secondSavedRecord.getRootPartitionKey2()).isEqualTo("existing-rootPk2_2"); + assertThat(secondSavedRecord.getRootSortKey1()).isEqualTo("existing-rootSk1_2"); + assertThat(secondSavedRecord.getRootSortKey2()).isEqualTo("existing-rootSk2_2"); + + assertThat(secondSavedRecord.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_2"); + assertThat(secondSavedRecord.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_2"); + assertThat(secondSavedRecord.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_2"); + assertThat(secondSavedRecord.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_2"); + + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() { + String tableName = "mixed-gsi-autogenerated-key-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); + enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + + BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + isValidUuid(result.getId()); + isValidUuid(result.getSort()); + isValidUuid(result.getRootPartitionKey1()); + isValidUuid(result.getRootPartitionKey2()); + isValidUuid(result.getRootSortKey1()); + isValidUuid(result.getRootSortKey2()); + isValidUuid(result.getFlattenedKeys().flattenedPartitionKey1); + isValidUuid(result.getFlattenedKeys().flattenedPartitionKey2); + isValidUuid(result.getFlattenedKeys().flattenedSortKey1); + isValidUuid(result.getFlattenedKeys().flattenedSortKey2); + + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void transactWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() { + String tableName = "mixed-gsi-autogenerated-key-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(1); + enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + + BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertThat(result.getId()).isEqualTo("existing-id_1"); + assertThat(result.getSort()).isEqualTo("existing-sort_1"); + assertThat(result.getRootPartitionKey1()).isEqualTo("existing-rootPk1_1"); + assertThat(result.getRootPartitionKey2()).isEqualTo("existing-rootPk2_1"); + assertThat(result.getRootSortKey1()).isEqualTo("existing-rootSk1_1"); + assertThat(result.getRootSortKey2()).isEqualTo("existing-rootSk2_1"); + assertThat(result.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_1"); + assertThat(result.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_1"); + assertThat(result.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_1"); + assertThat(result.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_1"); + + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + + private static BeanWithAutogeneratedKey buildBeanWithAutogeneratedKeyAndKeysPopulated() { + return buildBeanWithAutogeneratedKeyAndKeysPopulated(null); + } + + private static BeanWithAutogeneratedKey buildBeanWithAutogeneratedKeyAndKeysPopulated(Integer index) { + String suffix = index == null ? "" : "_" + index; + + BeanWithAutogeneratedKey record = new BeanWithAutogeneratedKey(); + record.setId("existing-id" + suffix); + record.setSortKey("existing-sortKey" + suffix); + record.setGsiPk("existing-gsiPk" + suffix); + record.setGsiSk("existing-gsiSk" + suffix); + + return record; + } + + private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated() { + return buildBeanWithCompositeGsiAndKeysPopulated(null); + } + + private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated(Integer index) { + String suffix = index == null ? "" : "_" + index; + + BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); + record.setId("existing-id" + suffix); + record.setSort("existing-sort" + suffix); + record.setRootPartitionKey1("existing-rootPk1" + suffix); + record.setRootPartitionKey2("existing-rootPk2" + suffix); + record.setRootSortKey1("existing-rootSk1" + suffix); + record.setRootSortKey2("existing-rootSk2" + suffix); + + BeanWithMixedCompositeGsi.FlattenedKeys flattenedKeys = new BeanWithMixedCompositeGsi.FlattenedKeys(); + flattenedKeys.setFlattenedPartitionKey1("existing-flattenedPk1" + suffix); + flattenedKeys.setFlattenedPartitionKey2("existing-flattenedPk2" + suffix); + flattenedKeys.setFlattenedSortKey1("existing-flattenedSk1" + suffix); + flattenedKeys.setFlattenedSortKey2("existing-flattenedSk2" + suffix); + record.setFlattenedKeys(flattenedKeys); + + return record; + } + + @DynamoDbBean - public static class BeanRecord { + public static class BeanWithoutAutogeneratedKey { private String id; private String sortKey; private String gsiPk; @@ -408,7 +757,7 @@ public void setData(String data) { } @DynamoDbBean - public static class AutogeneratedKeyRecord { + public static class BeanWithAutogeneratedKey { private String id; private String sortKey; private String gsiPk; @@ -458,7 +807,7 @@ public void setGsiSk(String gsiSk) { } @DynamoDbBean - public static class AutogeneratedKeyVersionedRecord { + public static class BeanWithAutogeneratedKeyAndVersion { private String id; private Long version; private String data; @@ -492,7 +841,7 @@ public void setData(String data) { } @DynamoDbBean - public static class AutogeneratedKeyInvalidTypeRecord { + public static class BeanWithAutogeneratedKeyOnAttributeWithInvalidType { private Integer id; @DynamoDbPartitionKey @@ -507,7 +856,7 @@ public void setId(Integer id) { } @DynamoDbBean - public static class AutogeneratedKeyOnNonKeyAttributeRecord { + public static class BeanWithAutogeneratedKeyOnNonKeyAttribute { private String id; private String nonKeyAttribute; @@ -531,7 +880,7 @@ public void setNonKeyAttribute(String nonKeyAttribute) { } @DynamoDbBean - public static class AutogeneratedKeyConflictingRecord { + public static class BeanWithConflictingAnnotations { private String id; @DynamoDbPartitionKey @@ -545,6 +894,142 @@ public void setId(String id) { this.id = id; } } + + @DynamoDbBean + public static class BeanWithMixedCompositeGsi { + private String id; + private String sort; + private String rootPartitionKey1; + private String rootPartitionKey2; + private String rootSortKey1; + private String rootSortKey2; + private FlattenedKeys flattenedKeys; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + @DynamoDbAutoGeneratedKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = FIRST) + public String getRootPartitionKey1() { + return rootPartitionKey1; + } + + public void setRootPartitionKey1(String rootPartitionKey1) { + this.rootPartitionKey1 = rootPartitionKey1; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = SECOND) + public String getRootPartitionKey2() { + return rootPartitionKey2; + } + + public void setRootPartitionKey2(String rootPartitionKey2) { + this.rootPartitionKey2 = rootPartitionKey2; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = FIRST) + public String getRootSortKey1() { + return rootSortKey1; + } + + public void setRootSortKey1(String rootSortKey1) { + this.rootSortKey1 = rootSortKey1; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = SECOND) + public String getRootSortKey2() { + return rootSortKey2; + } + + public void setRootSortKey2(String rootSortKey2) { + this.rootSortKey2 = rootSortKey2; + } + + @DynamoDbFlatten + public FlattenedKeys getFlattenedKeys() { + return flattenedKeys; + } + + public void setFlattenedKeys(FlattenedKeys flattenedKeys) { + this.flattenedKeys = flattenedKeys; + } + + @DynamoDbBean + public static class FlattenedKeys { + private String flattenedPartitionKey1; + private String flattenedPartitionKey2; + private String flattenedSortKey1; + private String flattenedSortKey2; + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.THIRD) + public String getFlattenedPartitionKey1() { + return flattenedPartitionKey1; + } + + public void setFlattenedPartitionKey1(String flattenedPartitionKey1) { + this.flattenedPartitionKey1 = flattenedPartitionKey1; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.FOURTH) + public String getFlattenedPartitionKey2() { + return flattenedPartitionKey2; + } + + public void setFlattenedPartitionKey2(String flattenedPartitionKey2) { + this.flattenedPartitionKey2 = flattenedPartitionKey2; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.THIRD) + public String getFlattenedSortKey1() { + return flattenedSortKey1; + } + + public void setFlattenedSortKey1(String flattenedSortKey1) { + this.flattenedSortKey1 = flattenedSortKey1; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.FOURTH) + public String getFlattenedSortKey2() { + return flattenedSortKey2; + } + + public void setFlattenedSortKey2(String flattenedSortKey2) { + this.flattenedSortKey2 = flattenedSortKey2; + } + } + } } From e654ef5c681d6aadb6af51909c6cacd250c8cad9 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Mon, 16 Feb 2026 19:09:56 +0200 Subject: [PATCH 11/14] Added tests with composite gsi --- .../extensions/AutoGeneratedKeyExtension.java | 27 +- .../annotations/DynamoDbAutoGeneratedKey.java | 2 - .../AutoGeneratedKeyCompositeGsiTest.java | 413 +++++++++++++++ .../AutoGeneratedKeyExtensionTest.java | 488 +----------------- 4 files changed, 433 insertions(+), 497 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java index e06bce6cb643..4c3ec6e5aa27 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java @@ -27,14 +27,13 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; -import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; -import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; +import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; @@ -165,12 +164,19 @@ private static void validateNoAutoGeneratedAnnotationConflict(TableMetadata tabl private void validateAutoGeneratedKeyPlacement(TableMetadata tableMetadata, Collection taggedAttributeNames) { - Set allowedKeyAttributes = - allowedKeysCache.computeIfAbsent(tableMetadata, metadata -> - new HashSet<>(metadata.keyAttributes() - .stream() - .map(KeyAttributeMetadata::name) - .collect(Collectors.toSet()))); + Set allowedKeyAttributes = allowedKeysCache.computeIfAbsent(tableMetadata, metadata -> { + + // Add primary keys + Set keyAttributes = new HashSet<>(metadata.primaryKeys()); + + // Add all secondary index keys + metadata.indices().stream().map(IndexMetadata::name).forEach(indexName -> { + keyAttributes.addAll(metadata.indexPartitionKeys(indexName)); + keyAttributes.addAll(metadata.indexSortKeys(indexName)); + }); + + return keyAttributes; + }); taggedAttributeNames.stream() .filter(attrName -> !allowedKeyAttributes.contains(attrName)) @@ -241,9 +247,8 @@ public void validateType(String attributeName, @Override public Consumer modifyMetadata(String attributeName, AttributeValueType attributeValueType) { - // Records attribute names annotated with @DynamoDbAutoGeneratedKey for lookup during write processing. - return metadata -> metadata.addCustomMetadataObject( - CUSTOM_METADATA_KEY, Collections.singleton(attributeName)); + return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, Collections.singleton(attributeName)) + .markAttributeAsKey(attributeName, attributeValueType); } } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java index cdad8d30616b..302a72df76c4 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java @@ -22,9 +22,7 @@ import java.lang.annotation.Target; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.AutoGeneratedKeyTag; -import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; /** * Marks a key attribute to be automatically populated with a UUID when the value is null or empty during a write operation. diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java new file mode 100644 index 000000000000..63477ecaa9f9 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java @@ -0,0 +1,413 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.extensions; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.FIRST; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.SECOND; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedKey; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; +import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch; + +public class AutoGeneratedKeyCompositeGsiTest extends LocalDynamoDbSyncTestBase { + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(BeanWithMixedCompositeGsi.class); + + private final DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(Stream.concat( + ExtensionResolver.defaultExtensions().stream(), + Stream.of(AutoGeneratedKeyExtension.create())) + .collect(Collectors.toList())) + .build(); + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName("mixed-gsi-autogenerated-key-table"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName( + getConcreteTableName("mixed-gsi-autogenerated-key-table"))); + } + + @Test + public void putItem_whenKeysNotPopulated_generatesNewUuids() { + BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); + + mappedTable.putItem(record); + BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream() + .findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertAllKeysAreValidUuids(result); + } + + @Test + public void putItem_whenKeysAlreadyPopulated_preservesExistingUuids() { + BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); + mappedTable.putItem(record); + + BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream() + .findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertCompositeKeyValuesArePreserved(result); + } + + @Test + public void updateItem_onBeanWithCompositeKeys_respectsUpdateBehavior() { + BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); + + // put item with all keys populated + mappedTable.putItem(record); + BeanWithMixedCompositeGsi afterPut = + mappedTable.scan().items().stream() + .findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + String originalPk = afterPut.getId(); + String originalSk = afterPut.getSort(); + String originalRootPartitionKey1 = afterPut.getRootPartitionKey1(); + String originalRootPartitionKey2 = afterPut.getRootPartitionKey2(); + String originalRootSortKey1 = afterPut.getRootSortKey1(); + String originalRootSortKey2 = afterPut.getRootSortKey2(); + String originalFlattenedPartitionKey1 = afterPut.getFlattenedKeys().flattenedPartitionKey1; + String originalFlattenedPartitionKey2 = afterPut.getFlattenedKeys().flattenedPartitionKey2; + String originalFlattenedSortKey1 = afterPut.getFlattenedKeys().flattenedSortKey1; + String originalFlattenedSortKey2 = afterPut.getFlattenedKeys().flattenedSortKey2; + + // update item + BeanWithMixedCompositeGsi update = new BeanWithMixedCompositeGsi(); + update.setId(afterPut.getId()); + update.setSort(afterPut.getSort()); + + mappedTable.updateItem(update); + BeanWithMixedCompositeGsi afterUpdate = + mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSort()))); + + + assertAllKeysAreValidUuids(afterUpdate); + + // id and sort preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys + assertThat(afterUpdate.getId()).isEqualTo(originalPk); + assertThat(afterUpdate.getSort()).isEqualTo(originalSk); + + // rootPartitionKey1, rootSortKey1, flattenedPartitionKey1, flattenedSortKey1 have WRITE_ALWAYS + // -> regenerates UUID on every update + assertThat(afterUpdate.getRootPartitionKey1()).isNotEqualTo(originalRootPartitionKey1); + assertThat(afterUpdate.getRootSortKey1()).isNotEqualTo(originalRootSortKey1); + assertThat(afterUpdate.getFlattenedKeys().flattenedPartitionKey1).isNotEqualTo(originalFlattenedPartitionKey1); + assertThat(afterUpdate.getFlattenedKeys().flattenedSortKey1).isNotEqualTo(originalFlattenedSortKey1); + + // rootPartitionKey2, rootSortKey2, flattenedPartitionKey2, flattenedSortKey2 have WRITE_IF_NOT_EXISTS + // -> preserves original UUID, only writes if null + assertThat(afterUpdate.getRootPartitionKey2()).isEqualTo(originalRootPartitionKey2); + assertThat(afterUpdate.getRootSortKey2()).isEqualTo(originalRootSortKey2); + assertThat(afterUpdate.getFlattenedKeys().getFlattenedPartitionKey2()).isEqualTo(originalFlattenedPartitionKey2); + assertThat(afterUpdate.getFlattenedKeys().getFlattenedSortKey2()).isEqualTo(originalFlattenedSortKey2); + } + + @Test + public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() { + BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1); + BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch( + WriteBatch.builder(BeanWithMixedCompositeGsi.class) + .mappedTableResource(mappedTable) + .addPutItem(firstRecord) + .addPutItem(secondRecord) + .build())); + + List results = mappedTable.scan().items().stream().collect(Collectors.toList()); + + assertThat(results.size()).isEqualTo(2); + assertAllKeysAreValidUuids(results.get(0)); + assertAllKeysAreValidUuids(results.get(1)); + } + + @Test + public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() { + BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1); + BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2); + + enhancedClient.batchWriteItem(req -> req.addWriteBatch(WriteBatch.builder(BeanWithMixedCompositeGsi.class) + .mappedTableResource(mappedTable) + .addPutItem(firstRecord) + .addPutItem(secondRecord) + .build())); + + BeanWithMixedCompositeGsi firstSavedRecord = + mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_1").sortValue("existing-sort_1"))); + assertCompositeKeyValuesArePreserved(firstSavedRecord, 1); + + BeanWithMixedCompositeGsi secondSavedRecord = + mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_2").sortValue("existing-sort_2"))); + assertCompositeKeyValuesArePreserved(secondSavedRecord, 2); + } + + @Test + public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() { + BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); + enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + + BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertAllKeysAreValidUuids(result); + } + + @Test + public void transactWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() { + BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); + enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() + .addPutItem(mappedTable, record) + .build()); + + BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertCompositeKeyValuesArePreserved(result); + } + + private static void assertAllKeysAreValidUuids(BeanWithMixedCompositeGsi record) { + isValidUuid(record.getId()); + isValidUuid(record.getSort()); + isValidUuid(record.getRootPartitionKey1()); + isValidUuid(record.getRootPartitionKey2()); + isValidUuid(record.getRootSortKey1()); + isValidUuid(record.getRootSortKey2()); + isValidUuid(record.getFlattenedKeys().flattenedPartitionKey1); + isValidUuid(record.getFlattenedKeys().flattenedPartitionKey2); + isValidUuid(record.getFlattenedKeys().flattenedSortKey1); + isValidUuid(record.getFlattenedKeys().flattenedSortKey2); + } + + private static void assertCompositeKeyValuesArePreserved(BeanWithMixedCompositeGsi actual) { + assertCompositeKeyValuesArePreserved(actual, null); + } + + private static void assertCompositeKeyValuesArePreserved(BeanWithMixedCompositeGsi actual, Integer recordIndex) { + String suffix = recordIndex == null ? "" : "_" + recordIndex; + + assertThat(actual.getId()).isEqualTo("existing-id" + suffix); + assertThat(actual.getSort()).isEqualTo("existing-sort" + suffix); + assertThat(actual.getRootPartitionKey1()).isEqualTo("existing-rootPk1" + suffix); + assertThat(actual.getRootPartitionKey2()).isEqualTo("existing-rootPk2" + suffix); + assertThat(actual.getRootSortKey1()).isEqualTo("existing-rootSk1" + suffix); + assertThat(actual.getRootSortKey2()).isEqualTo("existing-rootSk2" + suffix); + + assertThat(actual.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1" + suffix); + assertThat(actual.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2" + suffix); + assertThat(actual.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1" + suffix); + assertThat(actual.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2" + suffix); + } + + private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated() { + return buildBeanWithCompositeGsiAndKeysPopulated(null); + } + + private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated(Integer index) { + String suffix = index == null ? "" : "_" + index; + + BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); + record.setId("existing-id" + suffix); + record.setSort("existing-sort" + suffix); + record.setRootPartitionKey1("existing-rootPk1" + suffix); + record.setRootPartitionKey2("existing-rootPk2" + suffix); + record.setRootSortKey1("existing-rootSk1" + suffix); + record.setRootSortKey2("existing-rootSk2" + suffix); + + BeanWithMixedCompositeGsi.FlattenedKeys flattenedKeys = new BeanWithMixedCompositeGsi.FlattenedKeys(); + flattenedKeys.setFlattenedPartitionKey1("existing-flattenedPk1" + suffix); + flattenedKeys.setFlattenedPartitionKey2("existing-flattenedPk2" + suffix); + flattenedKeys.setFlattenedSortKey1("existing-flattenedSk1" + suffix); + flattenedKeys.setFlattenedSortKey2("existing-flattenedSk2" + suffix); + record.setFlattenedKeys(flattenedKeys); + + return record; + } + + @DynamoDbBean + public static class BeanWithMixedCompositeGsi { + private String id; + private String sort; + private String rootPartitionKey1; + private String rootPartitionKey2; + private String rootSortKey1; + private String rootSortKey2; + private FlattenedKeys flattenedKeys; + + @DynamoDbPartitionKey + @DynamoDbAutoGeneratedKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + @DynamoDbAutoGeneratedKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = FIRST) + public String getRootPartitionKey1() { + return rootPartitionKey1; + } + + public void setRootPartitionKey1(String rootPartitionKey1) { + this.rootPartitionKey1 = rootPartitionKey1; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = SECOND) + public String getRootPartitionKey2() { + return rootPartitionKey2; + } + + public void setRootPartitionKey2(String rootPartitionKey2) { + this.rootPartitionKey2 = rootPartitionKey2; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = FIRST) + public String getRootSortKey1() { + return rootSortKey1; + } + + public void setRootSortKey1(String rootSortKey1) { + this.rootSortKey1 = rootSortKey1; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = SECOND) + public String getRootSortKey2() { + return rootSortKey2; + } + + public void setRootSortKey2(String rootSortKey2) { + this.rootSortKey2 = rootSortKey2; + } + + @DynamoDbFlatten + public FlattenedKeys getFlattenedKeys() { + return flattenedKeys; + } + + public void setFlattenedKeys(FlattenedKeys flattenedKeys) { + this.flattenedKeys = flattenedKeys; + } + + @DynamoDbBean + public static class FlattenedKeys { + private String flattenedPartitionKey1; + private String flattenedPartitionKey2; + private String flattenedSortKey1; + private String flattenedSortKey2; + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.THIRD) + public String getFlattenedPartitionKey1() { + return flattenedPartitionKey1; + } + + public void setFlattenedPartitionKey1(String flattenedPartitionKey1) { + this.flattenedPartitionKey1 = flattenedPartitionKey1; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.FOURTH) + public String getFlattenedPartitionKey2() { + return flattenedPartitionKey2; + } + + public void setFlattenedPartitionKey2(String flattenedPartitionKey2) { + this.flattenedPartitionKey2 = flattenedPartitionKey2; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.THIRD) + public String getFlattenedSortKey1() { + return flattenedSortKey1; + } + + public void setFlattenedSortKey1(String flattenedSortKey1) { + this.flattenedSortKey1 = flattenedSortKey1; + } + + @DynamoDbAutoGeneratedKey + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.FOURTH) + public String getFlattenedSortKey2() { + return flattenedSortKey2; + } + + public void setFlattenedSortKey2(String flattenedSortKey2) { + this.flattenedSortKey2 = flattenedSortKey2; + } + } + } +} + + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java index 514bcea5d225..405f87daad95 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java @@ -18,8 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static software.amazon.awssdk.enhanced.dynamodb.UuidTestUtils.isValidUuid; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.FIRST; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.Order.SECOND; import java.util.List; import java.util.stream.Collectors; @@ -36,10 +34,8 @@ import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; -import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; @@ -285,7 +281,10 @@ public void putItem_whenAnnotationUsedOnNonKeyAttribute_throwsException() { try { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - assertThatThrownBy(() -> mappedTable.putItem(new BeanWithAutogeneratedKeyOnNonKeyAttribute())) + BeanWithAutogeneratedKeyOnNonKeyAttribute bean = new BeanWithAutogeneratedKeyOnNonKeyAttribute(); + bean.setId("id"); + + assertThatThrownBy(() -> mappedTable.putItem(bean)) .isInstanceOf(IllegalArgumentException.class) .hasMessage( "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " @@ -341,324 +340,6 @@ public void createBean_givenAutogeneratedKeyAnnotationAppliedOnNonStringAttribut } - // Tests with Mixed Composite Gsi - @Test - public void putItem_onBeanWithCompositeGsi_whenKeysNotPopulated_generatesNewUuids() { - String tableName = "mixed-gsi-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); - - try { - mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - - BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); - mappedTable.putItem(record); - - BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream() - .findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - - isValidUuid(result.getId()); - isValidUuid(result.getSort()); - isValidUuid(result.getRootPartitionKey1()); - isValidUuid(result.getRootPartitionKey2()); - isValidUuid(result.getRootSortKey1()); - isValidUuid(result.getRootSortKey2()); - isValidUuid(result.getFlattenedKeys().flattenedPartitionKey1); - isValidUuid(result.getFlattenedKeys().flattenedPartitionKey2); - isValidUuid(result.getFlattenedKeys().flattenedSortKey1); - isValidUuid(result.getFlattenedKeys().flattenedSortKey2); - - } finally { - try { - mappedTable.deleteTable(); - } catch (Exception ignored) { - } - } - } - - @Test - public void putItem_onBeanWithCompositeGsi_whenKeysAlreadyPopulated_preservesExistingUuids() { - String tableName = "mixed-gsi-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); - - try { - mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); - mappedTable.putItem(record); - - BeanWithMixedCompositeGsi result = - mappedTable.scan().items().stream() - .findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - - - assertThat(result.getId()).isEqualTo("existing-id"); - assertThat(result.getSort()).isEqualTo("existing-sort"); - assertThat(result.getRootPartitionKey1()).isEqualTo("existing-rootPk1"); - assertThat(result.getRootPartitionKey2()).isEqualTo("existing-rootPk2"); - assertThat(result.getRootSortKey1()).isEqualTo("existing-rootSk1"); - assertThat(result.getRootSortKey2()).isEqualTo("existing-rootSk2"); - assertThat(result.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1"); - assertThat(result.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2"); - assertThat(result.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1"); - assertThat(result.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2"); - - } finally { - try { - mappedTable.deleteTable(); - } catch (Exception ignored) { - } - } - } - - @Test - public void updateItem_onBeanWithCompositeKeys_respectsUpdateBehavior() { - String tableName = "mixed-gsi-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); - - try { - mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); - mappedTable.putItem(record); - - // put initial item - BeanWithMixedCompositeGsi afterPut = - mappedTable.scan().items().stream() - .findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - - String originalPk = afterPut.getId(); - String originalSk = afterPut.getSort(); - String originalRootPartitionKey1 = afterPut.getRootPartitionKey1(); - String originalRootPartitionKey2 = afterPut.getRootPartitionKey2(); - String originalRootSortKey1 = afterPut.getRootSortKey1(); - String originalRootSortKey2 = afterPut.getRootSortKey2(); - String originalFlattenedPartitionKey1 = afterPut.getFlattenedKeys().flattenedPartitionKey1; - String originalFlattenedPartitionKey2 = afterPut.getFlattenedKeys().flattenedPartitionKey2; - String originalFlattenedSortKey1 = afterPut.getFlattenedKeys().flattenedSortKey1; - String originalFlattenedSortKey2 = afterPut.getFlattenedKeys().flattenedSortKey2; - - // update item - BeanWithMixedCompositeGsi update = new BeanWithMixedCompositeGsi(); - update.setId(afterPut.getId()); - update.setSort(afterPut.getSort()); - - mappedTable.updateItem(update); - BeanWithMixedCompositeGsi afterUpdate = - mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSort()))); - - - // id and sort preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys - assertThat(afterUpdate.getId()).isEqualTo(originalPk); - assertThat(afterUpdate.getSort()).isEqualTo(originalSk); - - // rootPartitionKey1, rootSortKey1, flattenedPartitionKey1, flattenedSortKey1 have WRITE_ALWAYS - // -> regenerates UUID on every update - isValidUuid(afterUpdate.getRootPartitionKey1()); - isValidUuid(afterUpdate.getRootSortKey1()); - assertThat(afterUpdate.getRootPartitionKey1()).isNotEqualTo(originalRootPartitionKey1); - assertThat(afterUpdate.getRootSortKey1()).isNotEqualTo(originalRootSortKey1); - - isValidUuid(afterUpdate.getFlattenedKeys().flattenedPartitionKey1); - isValidUuid(afterUpdate.getFlattenedKeys().flattenedSortKey1); - assertThat(afterUpdate.getFlattenedKeys().flattenedPartitionKey1).isNotEqualTo(originalFlattenedPartitionKey1); - assertThat(afterUpdate.getFlattenedKeys().flattenedSortKey1).isNotEqualTo(originalFlattenedSortKey1); - - - // rootPartitionKey2, rootSortKey2, flattenedPartitionKey2, flattenedSortKey2 have WRITE_IF_NOT_EXISTS - // -> preserves original UUID, only writes if null - assertThat(afterUpdate.getRootPartitionKey2()).isEqualTo(originalRootPartitionKey2); - assertThat(afterUpdate.getRootSortKey2()).isEqualTo(originalRootSortKey2); - assertThat(afterUpdate.getFlattenedKeys().getFlattenedPartitionKey2()).isEqualTo(originalFlattenedPartitionKey2); - assertThat(afterUpdate.getFlattenedKeys().getFlattenedSortKey2()).isEqualTo(originalFlattenedSortKey2); - - } finally { - try { - mappedTable.deleteTable(); - } catch (Exception ignored) { - } - } - } - - @Test - public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() { - String tableName = "mixed-gsi-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); - - try { - mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1); - BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2); - - enhancedClient.batchWriteItem(req -> req.addWriteBatch( - WriteBatch.builder(BeanWithMixedCompositeGsi.class) - .mappedTableResource(mappedTable) - .addPutItem(firstRecord) - .addPutItem(secondRecord) - .build())); - - List results = mappedTable.scan().items().stream().collect(Collectors.toList()); - - assertThat(results.size()).isEqualTo(2); - isValidUuid(results.get(0).getId()); - isValidUuid(results.get(1).getId()); - isValidUuid(results.get(0).getSort()); - isValidUuid(results.get(1).getSort()); - isValidUuid(results.get(0).getRootPartitionKey1()); - isValidUuid(results.get(1).getRootPartitionKey1()); - isValidUuid(results.get(0).getRootPartitionKey2()); - isValidUuid(results.get(1).getRootPartitionKey2()); - isValidUuid(results.get(0).getRootSortKey1()); - isValidUuid(results.get(1).getRootSortKey1()); - isValidUuid(results.get(0).getRootSortKey2()); - isValidUuid(results.get(1).getRootSortKey2()); - isValidUuid(results.get(0).getFlattenedKeys().flattenedPartitionKey1); - isValidUuid(results.get(1).getFlattenedKeys().flattenedPartitionKey1); - isValidUuid(results.get(0).getFlattenedKeys().flattenedPartitionKey2); - isValidUuid(results.get(1).getFlattenedKeys().flattenedPartitionKey2); - isValidUuid(results.get(0).getFlattenedKeys().flattenedSortKey1); - isValidUuid(results.get(1).getFlattenedKeys().flattenedSortKey1); - isValidUuid(results.get(0).getFlattenedKeys().flattenedSortKey2); - isValidUuid(results.get(1).getFlattenedKeys().flattenedSortKey2); - - } finally { - try { - mappedTable.deleteTable(); - } catch (Exception ignored) { - } - } - } - - @Test - public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() { - String tableName = "mixed-gsi-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); - - try { - mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1); - BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2); - - enhancedClient.batchWriteItem(req -> req.addWriteBatch(WriteBatch.builder(BeanWithMixedCompositeGsi.class) - .mappedTableResource(mappedTable) - .addPutItem(firstRecord) - .addPutItem(secondRecord) - .build())); - - BeanWithMixedCompositeGsi firstSavedRecord = - mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_1").sortValue("existing-sort_1"))); - assertThat(firstSavedRecord.getId()).isEqualTo("existing-id_1"); - assertThat(firstSavedRecord.getSort()).isEqualTo("existing-sort_1"); - assertThat(firstSavedRecord.getRootPartitionKey1()).isEqualTo("existing-rootPk1_1"); - assertThat(firstSavedRecord.getRootPartitionKey2()).isEqualTo("existing-rootPk2_1"); - assertThat(firstSavedRecord.getRootSortKey1()).isEqualTo("existing-rootSk1_1"); - assertThat(firstSavedRecord.getRootSortKey2()).isEqualTo("existing-rootSk2_1"); - assertThat(firstSavedRecord.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_1"); - assertThat(firstSavedRecord.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_1"); - assertThat(firstSavedRecord.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_1"); - assertThat(firstSavedRecord.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_1"); - - - BeanWithMixedCompositeGsi secondSavedRecord = - mappedTable.getItem(r -> r.key(k -> k.partitionValue("existing-id_2").sortValue("existing-sort_2"))); - - assertThat(secondSavedRecord.getId()).isEqualTo("existing-id_2"); - assertThat(secondSavedRecord.getSort()).isEqualTo("existing-sort_2"); - assertThat(secondSavedRecord.getRootPartitionKey1()).isEqualTo("existing-rootPk1_2"); - assertThat(secondSavedRecord.getRootPartitionKey2()).isEqualTo("existing-rootPk2_2"); - assertThat(secondSavedRecord.getRootSortKey1()).isEqualTo("existing-rootSk1_2"); - assertThat(secondSavedRecord.getRootSortKey2()).isEqualTo("existing-rootSk2_2"); - - assertThat(secondSavedRecord.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_2"); - assertThat(secondSavedRecord.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_2"); - assertThat(secondSavedRecord.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_2"); - assertThat(secondSavedRecord.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_2"); - - } finally { - try { - mappedTable.deleteTable(); - } catch (Exception ignored) { - } - } - } - - @Test - public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() { - String tableName = "mixed-gsi-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); - - try { - mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - - BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); - enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() - .addPutItem(mappedTable, record) - .build()); - - BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - - isValidUuid(result.getId()); - isValidUuid(result.getSort()); - isValidUuid(result.getRootPartitionKey1()); - isValidUuid(result.getRootPartitionKey2()); - isValidUuid(result.getRootSortKey1()); - isValidUuid(result.getRootSortKey2()); - isValidUuid(result.getFlattenedKeys().flattenedPartitionKey1); - isValidUuid(result.getFlattenedKeys().flattenedPartitionKey2); - isValidUuid(result.getFlattenedKeys().flattenedSortKey1); - isValidUuid(result.getFlattenedKeys().flattenedSortKey2); - - } finally { - try { - mappedTable.deleteTable(); - } catch (Exception ignored) { - } - } - } - - @Test - public void transactWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() { - String tableName = "mixed-gsi-autogenerated-key-table"; - DynamoDbTable mappedTable = - enhancedClient.table(tableName, TableSchema.fromClass(BeanWithMixedCompositeGsi.class)); - - try { - mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(1); - enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() - .addPutItem(mappedTable, record) - .build()); - - BeanWithMixedCompositeGsi result = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - - assertThat(result.getId()).isEqualTo("existing-id_1"); - assertThat(result.getSort()).isEqualTo("existing-sort_1"); - assertThat(result.getRootPartitionKey1()).isEqualTo("existing-rootPk1_1"); - assertThat(result.getRootPartitionKey2()).isEqualTo("existing-rootPk2_1"); - assertThat(result.getRootSortKey1()).isEqualTo("existing-rootSk1_1"); - assertThat(result.getRootSortKey2()).isEqualTo("existing-rootSk2_1"); - assertThat(result.getFlattenedKeys().flattenedPartitionKey1).isEqualTo("existing-flattenedPk1_1"); - assertThat(result.getFlattenedKeys().flattenedPartitionKey2).isEqualTo("existing-flattenedPk2_1"); - assertThat(result.getFlattenedKeys().flattenedSortKey1).isEqualTo("existing-flattenedSk1_1"); - assertThat(result.getFlattenedKeys().flattenedSortKey2).isEqualTo("existing-flattenedSk2_1"); - - } finally { - try { - mappedTable.deleteTable(); - } catch (Exception ignored) { - } - } - } - - private static BeanWithAutogeneratedKey buildBeanWithAutogeneratedKeyAndKeysPopulated() { return buildBeanWithAutogeneratedKeyAndKeysPopulated(null); } @@ -675,31 +356,6 @@ private static BeanWithAutogeneratedKey buildBeanWithAutogeneratedKeyAndKeysPopu return record; } - private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated() { - return buildBeanWithCompositeGsiAndKeysPopulated(null); - } - - private static BeanWithMixedCompositeGsi buildBeanWithCompositeGsiAndKeysPopulated(Integer index) { - String suffix = index == null ? "" : "_" + index; - - BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); - record.setId("existing-id" + suffix); - record.setSort("existing-sort" + suffix); - record.setRootPartitionKey1("existing-rootPk1" + suffix); - record.setRootPartitionKey2("existing-rootPk2" + suffix); - record.setRootSortKey1("existing-rootSk1" + suffix); - record.setRootSortKey2("existing-rootSk2" + suffix); - - BeanWithMixedCompositeGsi.FlattenedKeys flattenedKeys = new BeanWithMixedCompositeGsi.FlattenedKeys(); - flattenedKeys.setFlattenedPartitionKey1("existing-flattenedPk1" + suffix); - flattenedKeys.setFlattenedPartitionKey2("existing-flattenedPk2" + suffix); - flattenedKeys.setFlattenedSortKey1("existing-flattenedSk1" + suffix); - flattenedKeys.setFlattenedSortKey2("existing-flattenedSk2" + suffix); - record.setFlattenedKeys(flattenedKeys); - - return record; - } - @DynamoDbBean public static class BeanWithoutAutogeneratedKey { @@ -894,142 +550,6 @@ public void setId(String id) { this.id = id; } } - - @DynamoDbBean - public static class BeanWithMixedCompositeGsi { - private String id; - private String sort; - private String rootPartitionKey1; - private String rootPartitionKey2; - private String rootSortKey1; - private String rootSortKey2; - private FlattenedKeys flattenedKeys; - - @DynamoDbPartitionKey - @DynamoDbAutoGeneratedKey - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - @DynamoDbSortKey - @DynamoDbAutoGeneratedKey - public String getSort() { - return sort; - } - - public void setSort(String sort) { - this.sort = sort; - } - - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) - @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = FIRST) - public String getRootPartitionKey1() { - return rootPartitionKey1; - } - - public void setRootPartitionKey1(String rootPartitionKey1) { - this.rootPartitionKey1 = rootPartitionKey1; - } - - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = SECOND) - public String getRootPartitionKey2() { - return rootPartitionKey2; - } - - public void setRootPartitionKey2(String rootPartitionKey2) { - this.rootPartitionKey2 = rootPartitionKey2; - } - - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) - @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = FIRST) - public String getRootSortKey1() { - return rootSortKey1; - } - - public void setRootSortKey1(String rootSortKey1) { - this.rootSortKey1 = rootSortKey1; - } - - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = SECOND) - public String getRootSortKey2() { - return rootSortKey2; - } - - public void setRootSortKey2(String rootSortKey2) { - this.rootSortKey2 = rootSortKey2; - } - - @DynamoDbFlatten - public FlattenedKeys getFlattenedKeys() { - return flattenedKeys; - } - - public void setFlattenedKeys(FlattenedKeys flattenedKeys) { - this.flattenedKeys = flattenedKeys; - } - - @DynamoDbBean - public static class FlattenedKeys { - private String flattenedPartitionKey1; - private String flattenedPartitionKey2; - private String flattenedSortKey1; - private String flattenedSortKey2; - - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) - @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.THIRD) - public String getFlattenedPartitionKey1() { - return flattenedPartitionKey1; - } - - public void setFlattenedPartitionKey1(String flattenedPartitionKey1) { - this.flattenedPartitionKey1 = flattenedPartitionKey1; - } - - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.FOURTH) - public String getFlattenedPartitionKey2() { - return flattenedPartitionKey2; - } - - public void setFlattenedPartitionKey2(String flattenedPartitionKey2) { - this.flattenedPartitionKey2 = flattenedPartitionKey2; - } - - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_ALWAYS) - @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.THIRD) - public String getFlattenedSortKey1() { - return flattenedSortKey1; - } - - public void setFlattenedSortKey1(String flattenedSortKey1) { - this.flattenedSortKey1 = flattenedSortKey1; - } - - @DynamoDbAutoGeneratedKey - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.FOURTH) - public String getFlattenedSortKey2() { - return flattenedSortKey2; - } - - public void setFlattenedSortKey2(String flattenedSortKey2) { - this.flattenedSortKey2 = flattenedSortKey2; - } - } - } } From 8c3d7d131db348dc9cb623343c542bb73de4b9ed Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Mon, 16 Feb 2026 19:25:40 +0200 Subject: [PATCH 12/14] Added tests with composite gsi --- .../extensions/AutoGeneratedKeyCompositeGsiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java index 63477ecaa9f9..541d3b80dad3 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java @@ -98,7 +98,7 @@ public void putItem_whenKeysAlreadyPopulated_preservesExistingUuids() { } @Test - public void updateItem_onBeanWithCompositeKeys_respectsUpdateBehavior() { + public void updateItem_respectsUpdateBehavior() { BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); // put item with all keys populated From 6d66b77e9ff2544c70f726e8b18c54cb73a0af48 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Mon, 16 Feb 2026 19:27:17 +0200 Subject: [PATCH 13/14] Added tests with composite gsi --- .../extensions/AutoGeneratedKeyCompositeGsiTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java index 541d3b80dad3..49a33d757e71 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java @@ -151,7 +151,7 @@ public void updateItem_respectsUpdateBehavior() { } @Test - public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() { + public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1); BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2); @@ -170,7 +170,7 @@ public void batchWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() { } @Test - public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() { + public void batchWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1); BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2); @@ -190,7 +190,7 @@ public void batchWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() } @Test - public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() { + public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { BeanWithMixedCompositeGsi record = new BeanWithMixedCompositeGsi(); enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() .addPutItem(mappedTable, record) @@ -203,7 +203,7 @@ public void transactWrite_onBean_whenKeysNotAlreadyPopulated_generatesNewUuids() } @Test - public void transactWrite_onBean_whenKeysAlreadyPopulated_preservesExistingUuids() { + public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { BeanWithMixedCompositeGsi record = buildBeanWithCompositeGsiAndKeysPopulated(); enhancedClient.transactWriteItems(TransactWriteItemsEnhancedRequest.builder() .addPutItem(mappedTable, record) From b935acf200b4b7b9c38f919fc56bd2d869037252 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 18 Feb 2026 16:31:11 +0200 Subject: [PATCH 14/14] Fixed assertions on generated uuids --- .../AutoGeneratedKeyCompositeGsiTest.java | 27 +++---- .../AutoGeneratedKeyExtensionTest.java | 74 ++++++++++++++----- .../AutoGeneratedUuidExtensionTest.java | 33 +++++---- 3 files changed, 85 insertions(+), 49 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java index 49a33d757e71..3b872f994ca2 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyCompositeGsiTest.java @@ -128,9 +128,6 @@ public void updateItem_respectsUpdateBehavior() { BeanWithMixedCompositeGsi afterUpdate = mappedTable.getItem(r -> r.key(k -> k.partitionValue(afterPut.getId()).sortValue(afterPut.getSort()))); - - assertAllKeysAreValidUuids(afterUpdate); - // id and sort preserve original values as DynamoDbUpdateBehavior has no effect on primary partition keys or sort keys assertThat(afterUpdate.getId()).isEqualTo(originalPk); assertThat(afterUpdate.getSort()).isEqualTo(originalSk); @@ -152,8 +149,8 @@ public void updateItem_respectsUpdateBehavior() { @Test public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { - BeanWithMixedCompositeGsi firstRecord = buildBeanWithCompositeGsiAndKeysPopulated(1); - BeanWithMixedCompositeGsi secondRecord = buildBeanWithCompositeGsiAndKeysPopulated(2); + BeanWithMixedCompositeGsi firstRecord = new BeanWithMixedCompositeGsi(); + BeanWithMixedCompositeGsi secondRecord = new BeanWithMixedCompositeGsi(); enhancedClient.batchWriteItem(req -> req.addWriteBatch( WriteBatch.builder(BeanWithMixedCompositeGsi.class) @@ -216,16 +213,16 @@ public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { } private static void assertAllKeysAreValidUuids(BeanWithMixedCompositeGsi record) { - isValidUuid(record.getId()); - isValidUuid(record.getSort()); - isValidUuid(record.getRootPartitionKey1()); - isValidUuid(record.getRootPartitionKey2()); - isValidUuid(record.getRootSortKey1()); - isValidUuid(record.getRootSortKey2()); - isValidUuid(record.getFlattenedKeys().flattenedPartitionKey1); - isValidUuid(record.getFlattenedKeys().flattenedPartitionKey2); - isValidUuid(record.getFlattenedKeys().flattenedSortKey1); - isValidUuid(record.getFlattenedKeys().flattenedSortKey2); + assertThat(isValidUuid(record.getId())).isTrue(); + assertThat(isValidUuid(record.getSort())).isTrue(); + assertThat(isValidUuid(record.getRootPartitionKey1())).isTrue(); + assertThat(isValidUuid(record.getRootPartitionKey2())).isTrue(); + assertThat(isValidUuid(record.getRootSortKey1())).isTrue(); + assertThat(isValidUuid(record.getRootSortKey2())).isTrue(); + assertThat(isValidUuid(record.getFlattenedKeys().flattenedPartitionKey1)).isTrue(); + assertThat(isValidUuid(record.getFlattenedKeys().flattenedPartitionKey2)).isTrue(); + assertThat(isValidUuid(record.getFlattenedKeys().flattenedSortKey1)).isTrue(); + assertThat(isValidUuid(record.getFlattenedKeys().flattenedSortKey2)).isTrue(); } private static void assertCompositeKeyValuesArePreserved(BeanWithMixedCompositeGsi actual) { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java index 405f87daad95..bc9d4cee5d29 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedKeyExtensionTest.java @@ -79,10 +79,10 @@ public void putItem_whenKeysNotPopulated_generatesNewUuids() { BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst() .orElseThrow(() -> new AssertionError("No record found")); - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); - isValidUuid(result.getGsiPk()); - isValidUuid(result.getGsiSk()); + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); + assertThat(isValidUuid(result.getGsiPk())).isTrue(); + assertThat(isValidUuid(result.getGsiSk())).isTrue(); } @Test @@ -126,7 +126,7 @@ public void updateItem_respectsUpdateBehavior() { assertThat(afterUpdate.getSortKey()).isEqualTo(originalSk); // gsiPk has WRITE_ALWAYS -> regenerates UUID on every update - isValidUuid(afterUpdate.getGsiPk()); + assertThat(isValidUuid(afterUpdate.getGsiPk())).isTrue(); assertThat(afterUpdate.getGsiPk()).isNotEqualTo(originalGsiPk); // gsiSk has WRITE_IF_NOT_EXISTS -> preserves original UUID, only writes if null @@ -147,10 +147,10 @@ public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { List results = mappedTable.scan().items().stream().collect(Collectors.toList()); assertThat(results.size()).isEqualTo(2); - isValidUuid(results.get(0).getId()); - isValidUuid(results.get(1).getId()); - isValidUuid(results.get(0).getSortKey()); - isValidUuid(results.get(1).getSortKey()); + assertThat(isValidUuid(results.get(0).getId())).isTrue(); + assertThat(isValidUuid(results.get(1).getId())).isTrue(); + assertThat(isValidUuid(results.get(0).getSortKey())).isTrue(); + assertThat(isValidUuid(results.get(1).getSortKey())).isTrue(); } @Test @@ -192,10 +192,10 @@ public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { BeanWithAutogeneratedKey result = mappedTable.scan().items().stream().findFirst() .orElseThrow(() -> new AssertionError("No record found")); - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); - isValidUuid(result.getGsiPk()); - isValidUuid(result.getGsiSk()); + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); + assertThat(isValidUuid(result.getGsiPk())).isTrue(); + assertThat(isValidUuid(result.getGsiSk())).isTrue(); } @Test @@ -217,7 +217,43 @@ public void transactWrite_whenKeysAlreadyPopulated_preservesExistingUuids() { } @Test - public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() { + public void putItem_onVersionedRecord_andKeyNotPopulated_worksWithAutoGeneratedKeyAndGeneratesNewUuid() { + String tableName = "versioned-record-autogenerated-key-table"; + DynamoDbTable mappedTable = + enhancedClient.table(tableName, TableSchema.fromClass(BeanWithAutogeneratedKeyAndVersion.class)); + + try { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + BeanWithAutogeneratedKeyAndVersion record = new BeanWithAutogeneratedKeyAndVersion(); + record.setData("data-v1"); + mappedTable.putItem(record); // id not set, should be auto generated + + BeanWithAutogeneratedKeyAndVersion retrieved = + mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + String generatedUuid = retrieved.getId(); + assertThat(isValidUuid(generatedUuid)).isTrue(); + assertThat(retrieved.getData()).isEqualTo("data-v1"); + assertThat(retrieved.getVersion()).isEqualTo(1L); + + retrieved.setData("data-v2"); + BeanWithAutogeneratedKeyAndVersion updated = mappedTable.updateItem(retrieved); + assertThat(updated.getId()).isEqualTo(generatedUuid); + assertThat(updated.getData()).isEqualTo("data-v2"); + assertThat(updated.getVersion()).isEqualTo(2L); + + } finally { + try { + mappedTable.deleteTable(); + } catch (Exception ignored) { + } + } + } + + @Test + public void putItem_onVersionedRecord_andAlreadyPopulatedKey_worksWithAutoGeneratedKeyAndPreservesInitialValue() { String tableName = "versioned-record-autogenerated-key-table"; DynamoDbTable mappedTable = enhancedClient.table(tableName, TableSchema.fromClass(BeanWithAutogeneratedKeyAndVersion.class)); @@ -230,15 +266,17 @@ public void putItem_onVersionedRecord_worksWithAutoGeneratedKey() { record.setData("data-v1"); mappedTable.putItem(record); - BeanWithAutogeneratedKeyAndVersion retrieved = mappedTable.scan().items().stream().findFirst() - .orElseThrow(() -> new AssertionError("No record found")); - isValidUuid(retrieved.getId()); + BeanWithAutogeneratedKeyAndVersion retrieved = + mappedTable.scan().items().stream().findFirst() + .orElseThrow(() -> new AssertionError("No record found")); + + assertThat(retrieved.getId()).isEqualTo("id"); assertThat(retrieved.getData()).isEqualTo("data-v1"); assertThat(retrieved.getVersion()).isEqualTo(1L); retrieved.setData("data-v2"); BeanWithAutogeneratedKeyAndVersion updated = mappedTable.updateItem(retrieved); - isValidUuid(updated.getId()); + assertThat(updated.getId()).isEqualTo("id"); assertThat(updated.getData()).isEqualTo("data-v2"); assertThat(updated.getVersion()).isEqualTo(2L); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java index 974d638ed8d9..458e31368d30 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/extensions/AutoGeneratedUuidExtensionTest.java @@ -78,8 +78,8 @@ public void putItem_whenKeysNotAlreadyPopulated_generatesNewUuids() { RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() .orElseThrow(() -> new AssertionError("No record found")); - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); } @Test @@ -92,8 +92,8 @@ public void putItem_whenKeysAlreadyPopulated_replacesExistingUuids() { RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() .orElseThrow(() -> new AssertionError("No record found")); - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); assertThat(result.getId()).isNotEqualTo("existing-id"); assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); } @@ -112,10 +112,10 @@ public void batchWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { List results = mappedTable.scan().items().stream().collect(Collectors.toList()); assertThat(results.size()).isEqualTo(2); - isValidUuid(results.get(0).getId()); - isValidUuid(results.get(1).getId()); - isValidUuid(results.get(0).getSortKey()); - isValidUuid(results.get(1).getSortKey()); + assertThat(isValidUuid(results.get(0).getId())).isTrue(); + assertThat(isValidUuid(results.get(1).getId())).isTrue(); + assertThat(isValidUuid(results.get(0).getSortKey())).isTrue(); + assertThat(isValidUuid(results.get(1).getSortKey())).isTrue(); } @Test @@ -138,10 +138,11 @@ public void batchWrite_whenKeysAlreadyPopulated_generatesNewUuids() { List results = mappedTable.scan().items().stream().collect(Collectors.toList()); assertThat(results.size()).isEqualTo(2); - isValidUuid(results.get(0).getId()); - isValidUuid(results.get(1).getId()); - isValidUuid(results.get(0).getSortKey()); - isValidUuid(results.get(1).getSortKey()); + assertThat(results.size()).isEqualTo(2); + assertThat(isValidUuid(results.get(0).getId())).isTrue(); + assertThat(isValidUuid(results.get(1).getId())).isTrue(); + assertThat(isValidUuid(results.get(0).getSortKey())).isTrue(); + assertThat(isValidUuid(results.get(1).getSortKey())).isTrue(); assertThat(results.get(0).getId()).isNotEqualTo("existing-id-1"); assertThat(results.get(1).getId()).isNotEqualTo("existing-id-2"); @@ -160,8 +161,8 @@ public void transactWrite_whenKeysNotAlreadyPopulated_generatesNewUuids() { RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() .orElseThrow(() -> new AssertionError("No record found")); - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); } @Test @@ -177,8 +178,8 @@ public void transactWrite_whenKeysAlreadyPopulated_generatesNewUuids() { RecordWithAutogeneratedUuid result = mappedTable.scan().items().stream().findFirst() .orElseThrow(() -> new AssertionError("No record found")); - isValidUuid(result.getId()); - isValidUuid(result.getSortKey()); + assertThat(isValidUuid(result.getId())).isTrue(); + assertThat(isValidUuid(result.getSortKey())).isTrue(); assertThat(result.getId()).isNotEqualTo("existing-id"); assertThat(result.getSortKey()).isNotEqualTo("existing-sk"); }