diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json
new file mode 100644
index 000000000000..81c91da75550
--- /dev/null
+++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json
@@ -0,0 +1,6 @@
+{
+ "type": "feature",
+ "category": "Amazon DynamoDB Enhanced Client",
+ "contributor": "",
+ "description": "Added 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..4c3ec6e5aa27
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
@@ -0,0 +1,254 @@
+/*
+ * 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 java.util.Collections.newSetFromMap;
+
+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.concurrent.ConcurrentHashMap;
+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 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)
+ *
Primary sort key (SK)
+ *
Partition or sort keys of secondary indexes (GSI or LSI)
+ *
+ *
+ *
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 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 {
+
+ /**
+ * 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";
+
+ /**
+ * 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<>());
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Inserts a UUID for attributes tagged with {@code @DynamoDbAutoGeneratedKey} when the attribute is null or empty, preserving
+ * existing values.
+ *
+ *
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);
+
+ if (taggedAttributes == null) {
+ return WriteModification.builder().build();
+ }
+
+ TableMetadata tableMetadata = context.tableMetadata();
+ if (validatedSchemas.add(tableMetadata)) {
+ validateNoAutoGeneratedAnnotationConflict(tableMetadata);
+ validateAutoGeneratedKeyPlacement(tableMetadata, taggedAttributes);
+ }
+
+ Map itemToTransform = new HashMap<>(context.items());
+ taggedAttributes.forEach(attr -> insertUuidIfMissing(itemToTransform, attr));
+
+ return WriteModification.builder()
+ .transformedItem(Collections.unmodifiableMap(itemToTransform))
+ .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 -> {
+
+ // 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))
+ .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())) {
+ 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) {
+ 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/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
index d92db8c60bbd..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
@@ -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;
@@ -39,6 +44,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.
@@ -77,9 +90,25 @@
@SdkPublicApi
@ThreadSafe
public final class AutoGeneratedUuidExtension implements DynamoDbEnhancedClientExtension {
+
+ /**
+ * 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 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();
+ 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
+ * thread-safe access during concurrent write operations.
+ */
+ private final Set validatedSchemas = newSetFromMap(new ConcurrentHashMap<>());
private AutoGeneratedUuidExtension() {
}
@@ -109,6 +138,9 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
return WriteModification.builder().build();
}
+ TableMetadata metadata = context.tableMetadata();
+ validateNoAnnotationConflict(metadata);
+
Map itemToTransform = new HashMap<>(context.items());
customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key));
return WriteModification.builder()
@@ -116,6 +148,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,
+ AUTOGENERATED_KEY_ANNOTATION,
+ AUTOGENERATED_UUID_ANNOTATION);
+ }
+ }
+
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..4a04286e2ece
--- /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 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 firstAnnotationMetadataKey,
+ String secondAnnotationMetadataKey,
+ String firstAnnotationName,
+ String secondAnnotationName) {
+
+ Collection> attributesHavingFirstAnnotation =
+ tableMetadata.customMetadataObject(firstAnnotationMetadataKey, Collection.class).orElse(Collections.emptyList());
+
+ if (attributesHavingFirstAnnotation.isEmpty()) {
+ return;
+ }
+
+ Collection> attributesHavingSecondAnnotation =
+ tableMetadata.customMetadataObject(secondAnnotationMetadataKey, Collection.class).orElse(Collections.emptyList());
+
+ if (attributesHavingSecondAnnotation.isEmpty()) {
+ return;
+ }
+
+ attributesHavingFirstAnnotation
+ .stream()
+ .filter(attributesHavingSecondAnnotation::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/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..302a72df76c4
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedKey.java
@@ -0,0 +1,49 @@
+/*
+ * 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.annotations.BeanTableSchemaAttributeTag;
+
+/**
+ * Marks a key attribute to be automatically populated with a UUID when the value is null or empty during a write operation.
+ *
+ *
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.
+ *
+ *
Semantics: Generates a UUID using {@link java.util.UUID#randomUUID()}
+ * only when the attribute is absent. Existing values are preserved.
+ *
+ *
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.
+ *
+ *
Valid only for {@link String} attributes.
+ */
+@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/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)
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..0720ffa99d8c
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtensionTest.java
@@ -0,0 +1,457 @@
+/*
+ * 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 = "1";
+ 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.create();
+
+ /**
+ * Schema that places @DynamoDbAutoGeneratedKey on GSI key ("keyAttribute") -> the validation passes.
+ */
+ private static final StaticTableSchema ITEM_WITH_KEY_SCHEMA =
+ StaticTableSchema.builder(AutogeneratedKeyItem.class)
+ .newItemSupplier(AutogeneratedKeyItem::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(AutogeneratedKeyItem::getId)
+ .setter(AutogeneratedKeyItem::setId)
+ .addTag(primaryPartitionKey())
+ .addTag(autoGeneratedKeyAttribute()))
+ .addAttribute(String.class, a -> a.name("keyAttribute")
+ .getter(AutogeneratedKeyItem::getKeyAttribute)
+ .setter(AutogeneratedKeyItem::setKeyAttribute)
+ .tags(
+ secondaryPartitionKey("gsi_keys_only"),
+ autoGeneratedKeyAttribute()))
+ .addAttribute(String.class, a -> a.name("simpleString")
+ .getter(AutogeneratedKeyItem::getSimpleString)
+ .setter(AutogeneratedKeyItem::setSimpleString))
+ .build();
+
+ /**
+ * Schema that places @DynamoDbAutoGeneratedKey on a NON-KEY attribute -> triggers the exception.
+ */
+ private static final StaticTableSchema INVALID_NONKEY_AUTOGEN_SCHEMA =
+ StaticTableSchema.builder(AutogeneratedKeyItem.class)
+ .newItemSupplier(AutogeneratedKeyItem::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(AutogeneratedKeyItem::getId)
+ .setter(AutogeneratedKeyItem::setId)
+ .addTag(primaryPartitionKey()))
+ .addAttribute(String.class, a -> a.name("keyAttribute")
+ .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(AutogeneratedKeyItem::getSimpleString)
+ .setter(AutogeneratedKeyItem::setSimpleString))
+ .build();
+
+ /**
+ * Schema that places @DynamoDbAutoGeneratedKey on LSI key ("simpleString") -> the validation passes.
+ */
+ private static final StaticTableSchema LSI_SK_AUTOGEN_SCHEMA =
+ StaticTableSchema.builder(AutogeneratedKeyItem.class)
+ .newItemSupplier(AutogeneratedKeyItem::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(AutogeneratedKeyItem::getId)
+ .setter(AutogeneratedKeyItem::setId)
+ .addTag(primaryPartitionKey()))
+ .addAttribute(String.class, a -> a.name("keyAttribute")
+ .getter(AutogeneratedKeyItem::getKeyAttribute)
+ .setter(AutogeneratedKeyItem::setKeyAttribute))
+ .addAttribute(String.class, a -> a.name("simpleString")
+ .getter(AutogeneratedKeyItem::getSimpleString)
+ .setter(AutogeneratedKeyItem::setSimpleString)
+ .tags(
+ secondarySortKey("lsi1"),
+ autoGeneratedKeyAttribute()))
+ .build();
+
+ @Test
+ public void updateItem_withExistingKey_preservesValueAndDoesNotGenerateNewOne() {
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ 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));
+
+ assertThat(isValidUuid(transformed.get("keyAttribute").s())).isTrue();
+ assertThat(result.updateExpression()).isNull();
+ }
+
+ @Test
+ public void updateItem_withoutExistingKey_generatesNewUuid() {
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ 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() {
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ 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 nonStringAttributeAnnotatedWithAutoGeneratedKey_throwsIllegalArgumentException() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() ->
+ StaticTableSchema.builder(AutogeneratedKeyItem.class)
+ .newItemSupplier(AutogeneratedKeyItem::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(AutogeneratedKeyItem::getId)
+ .setter(AutogeneratedKeyItem::setId)
+ .addTag(primaryPartitionKey()))
+ .addAttribute(Integer.class, a -> a.name("intAttribute")
+ .getter(AutogeneratedKeyItem::getIntAttribute)
+ .setter(AutogeneratedKeyItem::setIntAttribute)
+ .addTag(autoGeneratedKeyAttribute()))
+ .addAttribute(String.class, a -> a.name("simpleString")
+ .getter(AutogeneratedKeyItem::getSimpleString)
+ .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.");
+ }
+
+ @Test
+ public void autoGeneratedKey_onSecondaryPartitionKey_generatesUuid() {
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ item.setId(RECORD_ID);
+
+ 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();
+ }
+
+ @Test
+ public void autoGeneratedKey_onSecondarySortKey_generatesUuid() {
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ item.setId(RECORD_ID);
+
+ 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();
+ }
+
+ @Test
+ public void autoGeneratedKey_onNonKeyAttribute_throwsIllegalArgumentException() {
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ item.setId(RECORD_ID);
+
+ 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())
+ )
+ .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
+ public void conflictingAnnotations_onSameAttribute_throwsIllegalArgumentException() {
+ // Create a schema with both @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid on the same attribute
+ StaticTableSchema conflictingSchema =
+ StaticTableSchema
+ .builder(AutogeneratedKeyItem.class)
+ .newItemSupplier(AutogeneratedKeyItem::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .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(AutogeneratedKeyItem::getSimpleString)
+ .setter(AutogeneratedKeyItem::setSimpleString))
+ .build();
+
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ 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(AutogeneratedKeyItem.class)
+ .newItemSupplier(AutogeneratedKeyItem::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(AutogeneratedKeyItem::getId)
+ .setter(AutogeneratedKeyItem::setId)
+ .addTag(primaryPartitionKey()))
+ .addAttribute(String.class, a -> a.name("keyAttribute")
+ .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(AutogeneratedKeyItem::getSimpleString)
+ .setter(AutogeneratedKeyItem::setSimpleString))
+ .build();
+
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ 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() {
+ StaticTableSchema conflictingSchema =
+ StaticTableSchema
+ .builder(AutogeneratedKeyItem.class)
+ .newItemSupplier(AutogeneratedKeyItem::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(AutogeneratedKeyItem::getId)
+ .setter(AutogeneratedKeyItem::setId)
+ .addTag(primaryPartitionKey())
+ .addTag(autoGeneratedKeyAttribute())
+ .addTag(autoGeneratedUuidAttribute()))
+ .build();
+
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ 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.");
+ }
+
+ @Test
+ public void beforeWrite_noAttributesTaggedWithAutogeneratedKey_returnsEmptyModification() {
+ StaticTableSchema schemaWithoutTags =
+ StaticTableSchema.builder(AutogeneratedKeyItem.class)
+ .newItemSupplier(AutogeneratedKeyItem::new)
+ .addAttribute(String.class, a -> a.name("id")
+ .getter(AutogeneratedKeyItem::getId)
+ .setter(AutogeneratedKeyItem::setId)
+ .addTag(primaryPartitionKey()))
+ .build();
+
+ AutogeneratedKeyItem item = new AutogeneratedKeyItem();
+ 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 AutogeneratedKeyItem {
+
+ private String id;
+ private String keyAttribute;
+ private String simpleString;
+ private Integer intAttribute;
+
+ AutogeneratedKeyItem() {
+ }
+
+ 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 AutogeneratedKeyItem)) {
+ return false;
+ }
+ AutogeneratedKeyItem that = (AutogeneratedKeyItem) 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..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
@@ -65,6 +65,34 @@ public class AutoGeneratedUuidExtensionTest {
.setter(ItemWithUuid::setSimpleString))
.build();
+ @Test
+ public void beforeWrite_withNoMetadata_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();
@@ -152,8 +180,35 @@ 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
+ public void beforeWrite_withoutMetadata_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) {
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutogeneratedConflictingAnnotationsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutogeneratedConflictingAnnotationsTest.java
new file mode 100644
index 000000000000..933bc9f64f86
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutogeneratedConflictingAnnotationsTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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 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.create();
+ 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(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())
+ .addTag(autoGeneratedUuidAttribute()))
+ .build();
+
+ @Test
+ public void autogeneratedKeyExtensionFirst_detectsConflictWithUuidExtension() {
+ RecordWithAutogenerated item = new RecordWithAutogenerated();
+ item.setAutogeneratedKeyField(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 '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 autogeneratedUuidExtensionFirst_detectsConflictWithKeyExtension() {
+ RecordWithAutogenerated item = new RecordWithAutogenerated();
+ item.setAutogeneratedKeyField(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 '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 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("autogeneratedUuidField")
+ .getter(RecordWithAutogenerated::getAutogeneratedUuidField)
+ .setter(RecordWithAutogenerated::setAutogeneratedUuidField)
+ .addTag(autoGeneratedUuidAttribute()))
+ .build();
+
+ RecordWithAutogenerated item = new RecordWithAutogenerated();
+
+ 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 RecordWithAutogenerated {
+ private String autogeneratedKeyField;
+ private String autogeneratedUuidField;
+
+ public String getAutogeneratedKeyField() {
+ return autogeneratedKeyField;
+ }
+
+ public void setAutogeneratedKeyField(String autogeneratedKeyField) {
+ this.autogeneratedKeyField = autogeneratedKeyField;
+ }
+
+ public String getAutogeneratedUuidField() {
+ return autogeneratedUuidField;
+ }
+
+ 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/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/extensions/VersionedRecordExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java
index 3f30fdc8ecdf..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
@@ -265,9 +265,9 @@ 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() {
@@ -375,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());
@@ -516,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);
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..2902465297f2
--- /dev/null
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedKeyRecordTest.java
@@ -0,0 +1,537 @@
+/*
+ * 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 java.util.List;
+import java.util.stream.Collectors;
+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;
+import software.amazon.awssdk.enhanced.dynamodb.model.TransactWriteItemsEnhancedRequest;
+import software.amazon.awssdk.enhanced.dynamodb.model.WriteBatch;
+
+/**
+ * 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.create())
+ .build()
+ .table(getConcreteTableName("AutoGenKey-table"), schema);
+ }
+
+ @Parameters(name = "{index}: {0}")
+ public static Collection