diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json new file mode 100644 index 000000000000..cab57e6fb0cf --- /dev/null +++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon DynamoDB Enhanced Client", + "contributor": "", + "description": "Added support for @DynamoDbAutoGeneratedTimestampAttribute and @DynamoDbUpdateBehavior on attributes within nested objects. The @DynamoDbUpdateBehavior annotation will only take effect for nested attributes when using IgnoreNullsMode.SCALAR_ONLY." +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java index 2ac27d918202..93425ab99f8b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java @@ -15,13 +15,21 @@ package software.amazon.awssdk.enhanced.dynamodb.extensions; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.getTableSchemaForListElement; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.reconstructCompositeKey; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.resolveSchemasPerPath; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.getNestedSchema; + import java.time.Clock; import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; +import java.util.stream.Collectors; import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -30,6 +38,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.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -64,6 +73,10 @@ *

* Every time a new update of the record is successfully written to the database, the timestamp at which it was modified will * be automatically updated. This extension applies the conversions as defined in the attribute convertor. + * The implementation handles both flattened nested parameters (identified by keys separated with + * {@code "_NESTED_ATTR_UPDATE_"}) and entire nested maps or lists, ensuring consistent behavior across both representations. + * If a nested object or list is {@code null}, no timestamp values will be generated for any of its annotated fields. + * The same timestamp value is used for both top-level attributes and all applicable nested fields. */ @SdkPublicApi @ThreadSafe @@ -126,26 +139,109 @@ public static AutoGeneratedTimestampRecordExtension create() { */ @Override public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) { + Map itemToTransform = new HashMap<>(context.items()); + + Map updatedItems = new HashMap<>(); + Instant currentInstant = clock.instant(); + + itemToTransform.forEach((key, value) -> { + if (value.hasM() && value.m() != null) { + Optional> nestedSchema = getNestedSchema(context.tableSchema(), key); + if (nestedSchema.isPresent()) { + Map processed = processNestedObject(value.m(), nestedSchema.get(), currentInstant); + updatedItems.put(key, AttributeValue.builder().m(processed).build()); + } + } else if (value.hasL() && !value.l().isEmpty() && value.l().get(0).hasM()) { + TableSchema elementListSchema = getTableSchemaForListElement(context.tableSchema(), key); + + List updatedList = value.l() + .stream() + .map(listItem -> listItem.hasM() ? + AttributeValue.builder() + .m(processNestedObject(listItem.m(), + elementListSchema, + currentInstant)) + .build() : listItem) + .collect(Collectors.toList()); + updatedItems.put(key, AttributeValue.builder().l(updatedList).build()); + } + }); + + Map> stringTableSchemaMap = resolveSchemasPerPath(itemToTransform, context.tableSchema()); - Collection customMetadataObject = context.tableMetadata() - .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class).orElse(null); + stringTableSchemaMap.forEach((path, schema) -> { + Collection customMetadataObject = schema.tableMetadata() + .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class) + .orElse(null); - if (customMetadataObject == null) { + if (customMetadataObject != null) { + customMetadataObject.forEach( + key -> insertTimestampInItemToTransform(updatedItems, reconstructCompositeKey(path, key), + schema.converterForAttribute(key), currentInstant)); + } + }); + + if (updatedItems.isEmpty()) { return WriteModification.builder().build(); } - Map itemToTransform = new HashMap<>(context.items()); - customMetadataObject.forEach( - key -> insertTimestampInItemToTransform(itemToTransform, key, - context.tableSchema().converterForAttribute(key))); + + itemToTransform.putAll(updatedItems); + return WriteModification.builder() .transformedItem(Collections.unmodifiableMap(itemToTransform)) .build(); } + private Map processNestedObject(Map nestedMap, TableSchema nestedSchema, + Instant currentInstant) { + Map updatedNestedMap = new HashMap<>(nestedMap); + Collection customMetadataObject = nestedSchema.tableMetadata() + .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class).orElse(null); + + if (customMetadataObject != null) { + customMetadataObject.forEach( + key -> insertTimestampInItemToTransform(updatedNestedMap, String.valueOf(key), + nestedSchema.converterForAttribute(key), currentInstant)); + } + + nestedMap.forEach((nestedKey, nestedValue) -> { + if (nestedValue.hasM()) { + Optional> childSchemaOptional = getNestedSchema(nestedSchema, nestedKey); + TableSchema schemaToUse = childSchemaOptional.isPresent() ? childSchemaOptional.get() : nestedSchema; + updatedNestedMap.put(nestedKey, + AttributeValue.builder() + .m(processNestedObject(nestedValue.m(), schemaToUse, currentInstant)) + .build()); + + } else if (nestedValue.hasL() && !nestedValue.l().isEmpty() + && nestedValue.l().get(0).hasM()) { + try { + TableSchema listElementSchema = TableSchema.fromClass( + Class.forName(nestedSchema.converterForAttribute(nestedKey) + .type().rawClassParameters().get(0).rawClass().getName())); + List updatedList = nestedValue + .l() + .stream() + .map(listItem -> listItem.hasM() ? + AttributeValue.builder() + .m(processNestedObject(listItem.m(), + listElementSchema, + currentInstant)).build() : listItem) + .collect(Collectors.toList()); + updatedNestedMap.put(nestedKey, AttributeValue.builder().l(updatedList).build()); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Class not found for field name: " + nestedKey, e); + } + } + }); + return updatedNestedMap; + } + private void insertTimestampInItemToTransform(Map itemToTransform, String key, - AttributeConverter converter) { - itemToTransform.put(key, converter.transformFrom(clock.instant())); + AttributeConverter converter, + Instant instant) { + itemToTransform.put(key, converter.transformFrom(instant)); } /** diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/utility/NestedRecordUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/utility/NestedRecordUtils.java new file mode 100644 index 000000000000..aa724a3a1442 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/utility/NestedRecordUtils.java @@ -0,0 +1,135 @@ +/* + * 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.utility; + +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.getNestedSchema; +import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public final class NestedRecordUtils { + + private static final Pattern NESTED_OBJECT_PATTERN = Pattern.compile(NESTED_OBJECT_UPDATE); + + private NestedRecordUtils() { + } + + /** + * Resolves and returns the {@link TableSchema} for the element type of a list attribute from the provided root schema. + *

+ * This method is useful when dealing with lists of nested objects in a DynamoDB-enhanced table schema, + * particularly in scenarios where the list is part of a flattened nested structure. + *

+ * If the provided key contains the nested object delimiter (e.g., {@code _NESTED_ATTR_UPDATE_}), the method traverses + * the nested hierarchy based on that path to locate the correct schema for the target attribute. + * Otherwise, it directly resolves the list element type from the root schema using reflection. + * + * @param rootSchema The root {@link TableSchema} representing the top-level entity. + * @param key The key representing the list attribute, either flat or nested (using a delimiter). + * @return The {@link TableSchema} representing the list element type of the specified attribute. + * @throws IllegalArgumentException If the list element class cannot be found via reflection. + */ + public static TableSchema getTableSchemaForListElement(TableSchema rootSchema, String key) { + TableSchema listElementSchema; + try { + if (!key.contains(NESTED_OBJECT_UPDATE)) { + listElementSchema = TableSchema.fromClass( + Class.forName(rootSchema.converterForAttribute(key).type().rawClassParameters().get(0).rawClass().getName())); + } else { + String[] parts = NESTED_OBJECT_PATTERN.split(key); + TableSchema currentSchema = rootSchema; + + for (int i = 0; i < parts.length - 1; i++) { + Optional> nestedSchema = getNestedSchema(currentSchema, parts[i]); + if (nestedSchema.isPresent()) { + currentSchema = nestedSchema.get(); + } + } + String attributeName = parts[parts.length - 1]; + listElementSchema = TableSchema.fromClass( + Class.forName(currentSchema.converterForAttribute(attributeName) + .type().rawClassParameters().get(0).rawClass().getName())); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Class not found for field name: " + key, e); + } + return listElementSchema; + } + + /** + * Traverses the attribute keys representing flattened nested structures and resolves the corresponding + * {@link TableSchema} for each nested path. + *

+ * The method constructs a mapping between each unique nested path (represented as dot-delimited strings) + * and the corresponding {@link TableSchema} object derived from the root schema. It supports resolving schemas + * for arbitrarily deep nesting, using the {@code _NESTED_ATTR_UPDATE_} pattern as a path delimiter. + *

+ * This is typically used in update or transformation flows where fields from nested objects are represented + * as flattened keys in the attribute map (e.g., {@code parent_NESTED_ATTR_UPDATE_child}). + * + * @param attributesToSet A map of flattened attribute keys to values, where keys may represent paths to nested attributes. + * @param rootSchema The root {@link TableSchema} of the top-level entity. + * @return A map where the key is the nested path (e.g., {@code "parent.child"}) and the value is the {@link TableSchema} + * corresponding to that level in the object hierarchy. + */ + public static Map> resolveSchemasPerPath(Map attributesToSet, + TableSchema rootSchema) { + Map> schemaMap = new HashMap<>(); + schemaMap.put("", rootSchema); + + for (String key : attributesToSet.keySet()) { + String[] parts = NESTED_OBJECT_PATTERN.split(key); + + StringBuilder pathBuilder = new StringBuilder(); + TableSchema currentSchema = rootSchema; + + for (int i = 0; i < parts.length - 1; i++) { + if (pathBuilder.length() > 0) { + pathBuilder.append("."); + } + pathBuilder.append(parts[i]); + + String path = pathBuilder.toString(); + + if (!schemaMap.containsKey(path)) { + Optional> nestedSchema = getNestedSchema(currentSchema, parts[i]); + if (nestedSchema.isPresent()) { + schemaMap.put(path, nestedSchema.get()); + currentSchema = nestedSchema.get(); + } + } else { + currentSchema = schemaMap.get(path); + } + } + } + return schemaMap; + } + + public static String reconstructCompositeKey(String path, String attributeName) { + if (path == null || path.isEmpty()) { + return attributeName; + } + return String.join(NESTED_OBJECT_UPDATE, path.split("\\.")) + + NESTED_OBJECT_UPDATE + attributeName; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java index 61d750e98a7e..abb85f532559 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java @@ -204,4 +204,15 @@ public static List getItemsFromSupplier(List> itemSupplierLis public static boolean isNullAttributeValue(AttributeValue attributeValue) { return attributeValue.nul() != null && attributeValue.nul(); } + + /** + * Retrieves the {@link TableSchema} for a nested attribute within the given parent schema. + * + * @param parentSchema the schema of the parent bean class + * @param attributeName the name of the nested attribute + * @return an {@link Optional} containing the nested attribute's {@link TableSchema}, or empty if unavailable + */ + public static Optional> getNestedSchema(TableSchema parentSchema, String attributeName) { + return parentSchema.converterForAttribute(attributeName).type().tableSchema(); + } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java index 0ffe361b5aed..c775b31cdf77 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java @@ -131,8 +131,7 @@ public UpdateItemRequest generateRequest(TableSchema tableSchema, Map keyAttributes = filterMap(itemMap, entry -> primaryKeys.contains(entry.getKey())); Map nonKeyAttributes = filterMap(itemMap, entry -> !primaryKeys.contains(entry.getKey())); - - Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, nonKeyAttributes); + Expression updateExpression = generateUpdateExpressionIfExist(tableSchema, transformation, nonKeyAttributes); Expression conditionExpression = generateConditionExpressionIfExist(transformation, request); Map expressionNames = coalesceExpressionNames(updateExpression, conditionExpression); @@ -275,7 +274,7 @@ public TransactWriteItem generateTransactWriteItem(TableSchema tableSchema, O * if there are attributes to be updated (most likely). If both exist, they are merged and the code generates a final * Expression that represent the result. */ - private Expression generateUpdateExpressionIfExist(TableMetadata tableMetadata, + private Expression generateUpdateExpressionIfExist(TableSchema tableSchema, WriteModification transformation, Map attributes) { UpdateExpression updateExpression = null; @@ -284,7 +283,7 @@ private Expression generateUpdateExpressionIfExist(TableMetadata tableMetadata, } if (!attributes.isEmpty()) { List nonRemoveAttributes = UpdateExpressionConverter.findAttributeNames(updateExpression); - UpdateExpression operationUpdateExpression = operationExpression(attributes, tableMetadata, nonRemoveAttributes); + UpdateExpression operationUpdateExpression = operationExpression(attributes, tableSchema, nonRemoveAttributes); if (updateExpression == null) { updateExpression = operationUpdateExpression; } else { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionUtils.java index 1d47400ab2e6..4ad1989d057d 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionUtils.java @@ -15,21 +15,24 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.update; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.getNestedSchema; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef; import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE; import static software.amazon.awssdk.utils.CollectionUtils.filterMap; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.UpdateBehaviorTag; import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; @@ -57,12 +60,12 @@ public static String ifNotExists(String key, String initValue) { * Generates an UpdateExpression representing a POJO, with only SET and REMOVE actions. */ public static UpdateExpression operationExpression(Map itemMap, - TableMetadata tableMetadata, + TableSchema tableSchema, List nonRemoveAttributes) { Map setAttributes = filterMap(itemMap, e -> !isNullAttributeValue(e.getValue())); UpdateExpression setAttributeExpression = UpdateExpression.builder() - .actions(setActionsFor(setAttributes, tableMetadata)) + .actions(setActionsFor(setAttributes, tableSchema)) .build(); Map removeAttributes = @@ -78,13 +81,31 @@ public static UpdateExpression operationExpression(Map i /** * Creates a list of SET actions for all attributes supplied in the map. */ - private static List setActionsFor(Map attributesToSet, TableMetadata tableMetadata) { - return attributesToSet.entrySet() - .stream() - .map(entry -> setValue(entry.getKey(), - entry.getValue(), - UpdateBehaviorTag.resolveForAttribute(entry.getKey(), tableMetadata))) - .collect(Collectors.toList()); + private static List setActionsFor(Map attributesToSet, TableSchema tableSchema) { + List actions = new ArrayList<>(); + for (Map.Entry entry : attributesToSet.entrySet()) { + String key = entry.getKey(); + AttributeValue value = entry.getValue(); + + if (key.contains(NESTED_OBJECT_UPDATE)) { + TableSchema currentSchema = tableSchema; + List pathFieldNames = Arrays.asList(PATTERN.split(key)); + String attributeName = pathFieldNames.get(pathFieldNames.size() - 1); + + for (int i = 0; i < pathFieldNames.size() - 1; i++) { + Optional> nestedSchema = getNestedSchema(currentSchema, pathFieldNames.get(i)); + if (nestedSchema.isPresent()) { + currentSchema = nestedSchema.get(); + } + } + + actions.add(setValue(key, value, + UpdateBehaviorTag.resolveForAttribute(attributeName, currentSchema.tableMetadata()))); + } else { + actions.add(setValue(key, value, UpdateBehaviorTag.resolveForAttribute(key, tableSchema.tableMetadata()))); + } + } + return actions; } /** diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbUpdateBehavior.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbUpdateBehavior.java index fa161446c1a4..d14216b6a529 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbUpdateBehavior.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbUpdateBehavior.java @@ -22,10 +22,15 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanTableSchemaAttributeTags; import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; +import software.amazon.awssdk.enhanced.dynamodb.model.IgnoreNullsMode; /** * Specifies the behavior when this attribute is updated as part of an 'update' operation such as UpdateItem. See * documentation of {@link UpdateBehavior} for details on the different behaviors supported and the default behavior. + * For attributes within nested objects, this annotation is only respected when the request uses + * {@link IgnoreNullsMode#SCALAR_ONLY}. In {@link IgnoreNullsMode#MAPS_ONLY} or {@link IgnoreNullsMode#DEFAULT}, + * the annotation has no effect. When applied to a list of nested objects, the annotation is not supported, + * as individual elements cannot be updated — the entire list is replaced during an update operation. */ @SdkPublicApi @Target({ElementType.METHOD}) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/NestedRecordUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/NestedRecordUtilsTest.java new file mode 100644 index 000000000000..484819c04db1 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/NestedRecordUtilsTest.java @@ -0,0 +1,58 @@ +/* + * 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 software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.getTableSchemaForListElement; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.utility.NestedRecordUtils.resolveSchemasPerPath; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedRecordListElement; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedRecordWithUpdateBehavior; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class NestedRecordUtilsTest { + + @Test + public void getTableSchemaForListElement_shouldReturnElementSchema() { + TableSchema parentSchema = TableSchema.fromBean(NestedRecordWithUpdateBehavior.class); + + TableSchema childSchema = getTableSchemaForListElement(parentSchema, "nestedRecordList"); + + Assertions.assertNotNull(childSchema); + Assertions.assertEquals(TableSchema.fromBean(NestedRecordListElement.class), childSchema); + } + + @Test + public void resolveSchemasPerPath_shouldResolveNestedPaths() { + TableSchema rootSchema = TableSchema.fromBean(RecordWithUpdateBehaviors.class); + + Map attributesToSet = new HashMap<>(); + attributesToSet.put("nestedRecord_NESTED_ATTR_UPDATE_nestedRecord_NESTED_ATTR_UPDATE_attribute", + AttributeValue.builder().s("attributeValue").build()); + + Map> result = resolveSchemasPerPath(attributesToSet, rootSchema); + + Assertions.assertEquals(3, result.size()); + Assertions.assertTrue(result.containsKey("")); + Assertions.assertTrue(result.containsKey("nestedRecord")); + Assertions.assertTrue(result.containsKey("nestedRecord.nestedRecord")); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampRecordTest.java index 5d5ccf4fdb4b..937985e6c6c5 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampRecordTest.java @@ -15,7 +15,6 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; -import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; @@ -27,11 +26,9 @@ import java.time.Instant; import java.time.ZoneOffset; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; -import java.util.stream.IntStream; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -40,15 +37,12 @@ import org.mockito.Mockito; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.Expression; -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.converters.EpochMillisFormatTestConverter; import software.amazon.awssdk.enhanced.dynamodb.converters.TimeFormatUpdateTestConverter; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; -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.UpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; @@ -66,14 +60,9 @@ public class AutoGeneratedTimestampRecordTest extends LocalDynamoDbSyncTestBase public static final Instant MOCKED_INSTANT_UPDATE_ONE = Instant.now(Clock.fixed(Instant.parse("2019-01-14T14:00:00Z"), ZoneOffset.UTC)); - public static final Instant MOCKED_INSTANT_UPDATE_TWO = Instant.now(Clock.fixed(Instant.parse("2019-01-15T14:00:00Z"), ZoneOffset.UTC)); - private static final String TABLE_NAME = "table-name"; - private static final OperationContext PRIMARY_CONTEXT = - DefaultOperationContext.create(TABLE_NAME, TableMetadata.primaryIndexName()); - private static final TableSchema FLATTENED_TABLE_SCHEMA = StaticTableSchema.builder(FlattenedRecord.class) .newItemSupplier(FlattenedRecord::new) @@ -83,6 +72,69 @@ public class AutoGeneratedTimestampRecordTest extends LocalDynamoDbSyncTestBase .tags(autoGeneratedTimestampAttribute())) .build(); + private static final TableSchema nestedLevel4_SCHEMA = + StaticTableSchema.builder(NestedLevel4.class) + .newItemSupplier(NestedLevel4::new) + .addAttribute(Instant.class, a -> a.name("nestedLevel4TimeAttribute") + .getter(NestedLevel4::getNestedLevel4TimeAttribute) + .setter(NestedLevel4::setNestedLevel4TimeAttribute) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(String.class, a -> a.name("nestedLevel4Attribute") + .getter(NestedLevel4::getNestedLevel4Attribute) + .setter(NestedLevel4::setNestedLevel4Attribute)) + .build(); + + private static final TableSchema nestedLevel3_SCHEMA = + StaticTableSchema.builder(NestedLevel3.class) + .newItemSupplier(NestedLevel3::new) + .addAttribute(Instant.class, a -> a.name("nestedLevel3TimeAttribute") + .getter(NestedLevel3::getNestedLevel3TimeAttribute) + .setter(NestedLevel3::setNestedLevel3TimeAttribute) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(String.class, a -> a.name("nestedLevel3Attribute") + .getter(NestedLevel3::getNestedLevel3Attribute) + .setter(NestedLevel3::setNestedLevel3Attribute)) + .addAttribute( + EnhancedType.documentOf(NestedLevel4.class, nestedLevel4_SCHEMA), + a -> a.name("nestedLevel4") + .getter(NestedLevel3::getNestedLevel4) + .setter(NestedLevel3::setNestedLevel4)) + .build(); + + private static final TableSchema nestedLevel2_SCHEMA = + StaticTableSchema.builder(NestedLevel2.class) + .newItemSupplier(NestedLevel2::new) + .addAttribute(Instant.class, a -> a.name("nestedLevel2TimeAttribute") + .getter(NestedLevel2::getNestedLevel2TimeAttribute) + .setter(NestedLevel2::setNestedLevel2TimeAttribute) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(String.class, a -> a.name("nestedLevel2Attribute") + .getter(NestedLevel2::getNestedLevel2Attribute) + .setter(NestedLevel2::setNestedLevel2Attribute)) + .addAttribute( + EnhancedType.documentOf(NestedLevel3.class, nestedLevel3_SCHEMA), + a -> a.name("nestedLevel3") + .getter(NestedLevel2::getNestedLevel3) + .setter(NestedLevel2::setNestedLevel3)) + .build(); + + private static final TableSchema nestedLevel1_SCHEMA = + StaticTableSchema.builder(NestedLevel1.class) + .newItemSupplier(NestedLevel1::new) + .addAttribute(Instant.class, a -> a.name("nestedLevel1TimeAttribute") + .getter(NestedLevel1::getNestedLevel1TimeAttribute) + .setter(NestedLevel1::setNestedLevel1TimeAttribute) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(String.class, a -> a.name("nestedLevel1Attribute") + .getter(NestedLevel1::getNestedLevel1Attribute) + .setter(NestedLevel1::setNestedLevel1Attribute)) + .addAttribute( + EnhancedType.documentOf(NestedLevel2.class, nestedLevel2_SCHEMA), + a -> a.name("nestedLevel2") + .getter(NestedLevel1::getNestedLevel2) + .setter(NestedLevel1::setNestedLevel2)) + .build(); + private static final TableSchema TABLE_SCHEMA = StaticTableSchema.builder(Record.class) .newItemSupplier(Record::new) @@ -103,23 +155,23 @@ public class AutoGeneratedTimestampRecordTest extends LocalDynamoDbSyncTestBase .tags(autoGeneratedTimestampAttribute(), updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) .addAttribute(Instant.class, a -> a.name("lastUpdatedDateInEpochMillis") - .getter(Record::getLastUpdatedDateInEpochMillis) - .setter(Record::setLastUpdatedDateInEpochMillis) - .attributeConverter(EpochMillisFormatTestConverter.create()) - .tags(autoGeneratedTimestampAttribute())) + .getter(Record::getLastUpdatedDateInEpochMillis) + .setter(Record::setLastUpdatedDateInEpochMillis) + .attributeConverter(EpochMillisFormatTestConverter.create()) + .tags(autoGeneratedTimestampAttribute())) .addAttribute(Instant.class, a -> a.name("convertedLastUpdatedDate") .getter(Record::getConvertedLastUpdatedDate) .setter(Record::setConvertedLastUpdatedDate) .attributeConverter(TimeFormatUpdateTestConverter.create()) .tags(autoGeneratedTimestampAttribute())) .flatten(FLATTENED_TABLE_SCHEMA, Record::getFlattenedRecord, Record::setFlattenedRecord) + .addAttribute(EnhancedType.documentOf(NestedLevel1.class, + nestedLevel1_SCHEMA, + b -> b.ignoreNulls(true)), + a -> a.name("nestedRecord").getter(Record::getNestedRecord) + .setter(Record::setNestedRecord)) .build(); - private final List> fakeItems = - IntStream.range(0, 4) - .mapToObj($ -> createUniqueFakeItem()) - .map(fakeItem -> TABLE_SCHEMA.itemToMap(fakeItem, true)) - .collect(toList()); private final DynamoDbTable mappedTable; private final Clock mockCLock = Mockito.mock(Clock.class); @@ -160,39 +212,84 @@ public void deleteTable() { } @Test - public void putNewRecordSetsInitialAutoGeneratedTimestamp() { - Record item = new Record().setId("id").setAttribute("one"); + public void putNewRecord_setsInitialTimestamps_onAllNestedLevels() { + NestedLevel4 nestedLevel4 = new NestedLevel4().setNestedLevel4Attribute("attrL4"); + NestedLevel3 nestedLevel3 = new NestedLevel3().setNestedLevel3Attribute("attrL3").setNestedLevel4(nestedLevel4); + NestedLevel2 nestedLevel2 = new NestedLevel2().setNestedLevel2Attribute("attrL2").setNestedLevel3(nestedLevel3); + NestedLevel1 nestedLevel1 = new NestedLevel1().setNestedLevel1Attribute("attrL1").setNestedLevel2(nestedLevel2); + + Record item = new Record() + .setId("id") + .setAttribute("one") + .setNestedRecord(nestedLevel1); + mappedTable.putItem(r -> r.item(item)); Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); + GetItemResponse stored = getItemAsStoredFromDDB(); + + NestedLevel4 expL4 = new NestedLevel4() + .setNestedLevel4TimeAttribute(MOCKED_INSTANT_NOW).setNestedLevel4Attribute("attrL4"); + NestedLevel3 expL3 = new NestedLevel3() + .setNestedLevel3TimeAttribute(MOCKED_INSTANT_NOW).setNestedLevel3Attribute("attrL3").setNestedLevel4(expL4); + NestedLevel2 expL2 = new NestedLevel2() + .setNestedLevel2TimeAttribute(MOCKED_INSTANT_NOW).setNestedLevel2Attribute("attrL2").setNestedLevel3(expL3); + NestedLevel1 expL1 = new NestedLevel1() + .setNestedLevel1TimeAttribute(MOCKED_INSTANT_NOW).setNestedLevel1Attribute("attrL1").setNestedLevel2(expL2); + FlattenedRecord flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_NOW); - Record expectedRecord = new Record().setId("id") - .setAttribute("one") - .setLastUpdatedDate(MOCKED_INSTANT_NOW) - .setConvertedLastUpdatedDate(MOCKED_INSTANT_NOW) - .setCreatedDate(MOCKED_INSTANT_NOW) - .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_NOW) - .setFlattenedRecord(flattenedRecord); + + Record expectedRecord = new Record() + .setId("id") + .setAttribute("one") + .setLastUpdatedDate(MOCKED_INSTANT_NOW) + .setConvertedLastUpdatedDate(MOCKED_INSTANT_NOW) + .setCreatedDate(MOCKED_INSTANT_NOW) + .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_NOW) + .setFlattenedRecord(flattenedRecord) + .setNestedRecord(expL1); + assertThat(result, is(expectedRecord)); - // The data in DDB is stored in converted time format - assertThat(itemAsStoredInDDB.item().get("convertedLastUpdatedDate").s(), is("13 01 2019 14:00:00")); + assertThat(stored.item().get("convertedLastUpdatedDate").s(), is("13 01 2019 14:00:00")); + + // nestedLevel1 assertions + Map lvl1Map = stored.item().get("nestedRecord").m(); + assertThat(lvl1Map.get("nestedLevel1TimeAttribute").s(), is(MOCKED_INSTANT_NOW.toString())); + + // nestedLevel2 assertions + Map lvl2Map = lvl1Map.get("nestedLevel2").m(); + assertThat(lvl2Map.get("nestedLevel2TimeAttribute").s(), is(MOCKED_INSTANT_NOW.toString())); + + // nestedLevel3 assertions + Map lvl3Map = lvl2Map.get("nestedLevel3").m(); + assertThat(lvl3Map.get("nestedLevel3TimeAttribute").s(), is(MOCKED_INSTANT_NOW.toString())); + + // nestedLevel4 assertions + Map lvl4Map = lvl3Map.get("nestedLevel4").m(); + assertThat(lvl4Map.get("nestedLevel4TimeAttribute").s(), is(MOCKED_INSTANT_NOW.toString())); } @Test public void updateNewRecordSetsAutoFormattedDate() { - Record result = mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one"))); + Record result = mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one") + .setNestedRecord(new NestedLevel1() + .setNestedLevel1Attribute("attribute")))); GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); FlattenedRecord flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_NOW); + NestedLevel1 expectednestedLevel1 = new NestedLevel1().setNestedLevel1TimeAttribute(MOCKED_INSTANT_NOW) + .setNestedLevel1Attribute("attribute"); Record expectedRecord = new Record().setId("id") .setAttribute("one") .setLastUpdatedDate(MOCKED_INSTANT_NOW) .setConvertedLastUpdatedDate(MOCKED_INSTANT_NOW) .setCreatedDate(MOCKED_INSTANT_NOW) .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_NOW) - .setFlattenedRecord(flattenedRecord); + .setFlattenedRecord(flattenedRecord) + .setNestedRecord(expectednestedLevel1); assertThat(result, is(expectedRecord)); // The data in DDB is stored in converted time format assertThat(itemAsStoredInDDB.item().get("convertedLastUpdatedDate").s(), is("13 01 2019 14:00:00")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1TimeAttribute").s(), + is(MOCKED_INSTANT_NOW.toString())); } @Test @@ -415,6 +512,76 @@ public void incorrectTypeForAutoUpdateTimestampThrowsException(){ .build(); } + @Test + public void putItemFollowedByUpdatesShouldGenerateTimestampsOnNestedFields() { + mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one") + .setNestedRecord(new NestedLevel1().setNestedLevel1Attribute("attribute")))); + mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1Attribute").s(), is("attribute")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1TimeAttribute").s(), + is(MOCKED_INSTANT_NOW.toString())); + + //First Update + Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + + mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one") + .setNestedRecord(new NestedLevel1().setNestedLevel1Attribute( + "attribute1")))); + itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1Attribute").s(), is("attribute1")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1TimeAttribute").s(), + is(MOCKED_INSTANT_UPDATE_ONE.toString())); + + //Second Update + Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_TWO); + mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one") + .setNestedRecord(new NestedLevel1().setNestedLevel1Attribute( + "attribute2")))); + itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1Attribute").s(), is("attribute2")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1TimeAttribute").s(), + is(MOCKED_INSTANT_UPDATE_TWO.toString())); + } + + @Test + public void putItemFollowedByUpdatesShouldGenerateTimestampsOnNestedFieldsList() { + mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one") + .setNestedRecord(new NestedLevel1().setNestedLevel1Attribute("attribute")))); + mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1Attribute").s(), is("attribute")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1TimeAttribute").s(), + is(MOCKED_INSTANT_NOW.toString())); + + //First Update + Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + + mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one") + .setNestedRecord(new NestedLevel1().setNestedLevel1Attribute( + "attribute1")))); + itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1Attribute").s(), is("attribute1")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1TimeAttribute").s(), + is(MOCKED_INSTANT_UPDATE_ONE.toString())); + + //Second Update + Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_TWO); + mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one") + .setNestedRecord(new NestedLevel1().setNestedLevel1Attribute( + "attribute2")))); + itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1Attribute").s(), is("attribute2")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("nestedLevel1TimeAttribute").s(), + is(MOCKED_INSTANT_UPDATE_TWO.toString())); + } + private GetItemResponse getItemAsStoredFromDDB() { Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s("id").build()); @@ -424,6 +591,43 @@ private GetItemResponse getItemAsStoredFromDDB() { .consistentRead(true).build()); } + private static class FlattenedRecord { + private Instant generated; + + public Instant getGenerated() { + return generated; + } + + public FlattenedRecord setGenerated(Instant generated) { + this.generated = generated; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FlattenedRecord that = (FlattenedRecord) o; + return Objects.equals(generated, that.generated); + } + + @Override + public int hashCode() { + return Objects.hash(generated); + } + + @Override + public String toString() { + return "FlattenedRecord{" + + "generated=" + generated + + '}'; + } + } + private static class Record { private String id; private String attribute; @@ -432,6 +636,7 @@ private static class Record { private Instant convertedLastUpdatedDate; private Instant lastUpdatedDateInEpochMillis; private FlattenedRecord flattenedRecord; + private NestedLevel1 nestedLevel1; private String getId() { return id; @@ -496,6 +701,15 @@ public Record setFlattenedRecord(FlattenedRecord flattenedRecord) { return this; } + public NestedLevel1 getNestedRecord() { + return nestedLevel1; + } + + public Record setNestedRecord(NestedLevel1 nestedLevel1) { + this.nestedLevel1 = nestedLevel1; + return this; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -511,13 +725,14 @@ public boolean equals(Object o) { Objects.equals(createdDate, record.createdDate) && Objects.equals(lastUpdatedDateInEpochMillis, record.lastUpdatedDateInEpochMillis) && Objects.equals(convertedLastUpdatedDate, record.convertedLastUpdatedDate) && - Objects.equals(flattenedRecord, record.flattenedRecord); + Objects.equals(flattenedRecord, record.flattenedRecord) && + Objects.equals(nestedLevel1, record.nestedLevel1); } @Override public int hashCode() { return Objects.hash(id, attribute, lastUpdatedDate, createdDate, lastUpdatedDateInEpochMillis, - convertedLastUpdatedDate, flattenedRecord); + convertedLastUpdatedDate, flattenedRecord, nestedLevel1); } @Override @@ -530,43 +745,227 @@ public String toString() { ", convertedLastUpdatedDate=" + convertedLastUpdatedDate + ", lastUpdatedDateInEpochMillis=" + lastUpdatedDateInEpochMillis + ", flattenedRecord=" + flattenedRecord + + ", nestedRecord=" + nestedLevel1 + '}'; } } - private static class FlattenedRecord { - private Instant generated; + private static class NestedLevel1 { + private String nestedLevel1Attribute; + private Instant nestedLevel1TimeAttribute; + private NestedLevel2 nestedLevel2; - public Instant getGenerated() { - return generated; + public String getNestedLevel1Attribute() { + return nestedLevel1Attribute; } - public FlattenedRecord setGenerated(Instant generated) { - this.generated = generated; + public NestedLevel1 setNestedLevel1Attribute(String nestedLevel1Attribute) { + this.nestedLevel1Attribute = nestedLevel1Attribute; + return this; + } + + public Instant getNestedLevel1TimeAttribute() { + return nestedLevel1TimeAttribute; + } + + public NestedLevel1 setNestedLevel1TimeAttribute(Instant nestedLevel1TimeAttribute) { + this.nestedLevel1TimeAttribute = nestedLevel1TimeAttribute; + return this; + } + + public NestedLevel2 getNestedLevel2() { + return nestedLevel2; + } + + public NestedLevel1 setNestedLevel2(NestedLevel2 nestedLevel2) { + this.nestedLevel2 = nestedLevel2; return this; } @Override public boolean equals(Object o) { - if (this == o) { - return true; + if (o == null || getClass() != o.getClass()) { + return false; } + NestedLevel1 nestedLevel1 = (NestedLevel1) o; + return Objects.equals(nestedLevel1Attribute, nestedLevel1.nestedLevel1Attribute) && + Objects.equals(nestedLevel1TimeAttribute, nestedLevel1.nestedLevel1TimeAttribute) && + Objects.equals(nestedLevel2, nestedLevel1.nestedLevel2); + } + + @Override + public int hashCode() { + return Objects.hash(nestedLevel1Attribute, nestedLevel1TimeAttribute, nestedLevel2); + } + + @Override + public String toString() { + return "nestedLevel1{" + + "nestedLevel1Attribute='" + nestedLevel1Attribute + '\'' + + ", nestedLevel1TimeAttribute=" + nestedLevel1TimeAttribute + + ", nestedLevel2=" + nestedLevel2 + + '}'; + } + } + + private static class NestedLevel2 { + private String nestedLevel2Attribute; + private Instant nestedLevel2TimeAttribute; + private NestedLevel3 nestedLevel3; + + public String getNestedLevel2Attribute() { + return nestedLevel2Attribute; + } + + public NestedLevel2 setNestedLevel2Attribute(String nestedLevel2Attribute) { + this.nestedLevel2Attribute = nestedLevel2Attribute; + return this; + } + + public Instant getNestedLevel2TimeAttribute() { + return nestedLevel2TimeAttribute; + } + + public NestedLevel2 setNestedLevel2TimeAttribute(Instant nestedLevel2TimeAttribute) { + this.nestedLevel2TimeAttribute = nestedLevel2TimeAttribute; + return this; + } + + public NestedLevel3 getNestedLevel3() { + return nestedLevel3; + } + + public NestedLevel2 setNestedLevel3(NestedLevel3 nestedLevel3) { + this.nestedLevel3 = nestedLevel3; + return this; + } + + @Override + public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - FlattenedRecord that = (FlattenedRecord) o; - return Objects.equals(generated, that.generated); + NestedLevel2 nestedLevel2 = (NestedLevel2) o; + return Objects.equals(nestedLevel2Attribute, nestedLevel2.nestedLevel2Attribute) && + Objects.equals(nestedLevel2TimeAttribute, nestedLevel2.nestedLevel2TimeAttribute) && + Objects.equals(nestedLevel3, nestedLevel2.nestedLevel3); } @Override public int hashCode() { - return Objects.hash(generated); + return Objects.hash(nestedLevel2Attribute, nestedLevel2TimeAttribute, nestedLevel3); } @Override public String toString() { - return "FlattenedRecord{" + - "generated=" + generated + + return "nestedLevel2{" + + "nestedLevel2Attribute='" + nestedLevel2Attribute + '\'' + + ", nestedLevel2TimeAttribute=" + nestedLevel2TimeAttribute + + ", nestedLevel3=" + nestedLevel3 + + '}'; + } + } + + private static class NestedLevel3 { + private String nestedLevel3Attribute; + private Instant nestedLevel3TimeAttribute; + private NestedLevel4 nestedLevel4; + + public String getNestedLevel3Attribute() { + return nestedLevel3Attribute; + } + + public NestedLevel3 setNestedLevel3Attribute(String nestedLevel3Attribute) { + this.nestedLevel3Attribute = nestedLevel3Attribute; + return this; + } + + public Instant getNestedLevel3TimeAttribute() { + return nestedLevel3TimeAttribute; + } + + public NestedLevel3 setNestedLevel3TimeAttribute(Instant nestedLevel3TimeAttribute) { + this.nestedLevel3TimeAttribute = nestedLevel3TimeAttribute; + return this; + } + + public NestedLevel4 getNestedLevel4() { + return nestedLevel4; + } + + public NestedLevel3 setNestedLevel4(NestedLevel4 nestedLevel4) { + this.nestedLevel4 = nestedLevel4; + return this; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedLevel3 nestedLevel3 = (NestedLevel3) o; + return Objects.equals(nestedLevel3Attribute, nestedLevel3.nestedLevel3Attribute) && + Objects.equals(nestedLevel3TimeAttribute, nestedLevel3.nestedLevel3TimeAttribute) && + Objects.equals(nestedLevel4, nestedLevel3.nestedLevel4); + } + + @Override + public int hashCode() { + return Objects.hash(nestedLevel3Attribute, nestedLevel3TimeAttribute, nestedLevel4); + } + + @Override + public String toString() { + return "nestedLevel3{" + + "nestedLevel3Attribute='" + nestedLevel3Attribute + '\'' + + ", nestedLevel3TimeAttribute=" + nestedLevel3TimeAttribute + + ", nestedLevel4=" + nestedLevel4 + + '}'; + } + } + + private static class NestedLevel4 { + private String nestedLevel4Attribute; + private Instant nestedLevel4TimeAttribute; + + public String getNestedLevel4Attribute() { + return nestedLevel4Attribute; + } + + public NestedLevel4 setNestedLevel4Attribute(String nestedLevel4Attribute) { + this.nestedLevel4Attribute = nestedLevel4Attribute; + return this; + } + + public Instant getNestedLevel4TimeAttribute() { + return nestedLevel4TimeAttribute; + } + + public NestedLevel4 setNestedLevel4TimeAttribute(Instant nestedLevel4TimeAttribute) { + this.nestedLevel4TimeAttribute = nestedLevel4TimeAttribute; + return this; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedLevel4 nestedLevel4 = (NestedLevel4) o; + return Objects.equals(nestedLevel4Attribute, nestedLevel4.nestedLevel4Attribute) && + Objects.equals(nestedLevel4TimeAttribute, nestedLevel4.nestedLevel4TimeAttribute); + } + + @Override + public int hashCode() { + return Objects.hash(nestedLevel4Attribute, nestedLevel4TimeAttribute); + } + + @Override + public String toString() { + return "nestedLevel4{" + + "nestedLevel4Attribute='" + nestedLevel4Attribute + '\'' + + ", nestedLevel4TimeAttribute=" + nestedLevel4TimeAttribute + '}'; } } @@ -621,6 +1020,4 @@ public String toString() { '}'; } } - - } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java index 196d38282277..15342c3f25e5 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java @@ -2,9 +2,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.common.collect.ImmutableList; import java.time.Instant; import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.After; @@ -16,6 +20,7 @@ import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.CompositeRecord; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FlattenRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedRecordListElement; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedRecordWithUpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors; import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; @@ -62,11 +67,16 @@ public void deleteTable() { @Test public void updateBehaviors_firstUpdate() { - Instant currentTime = Instant.now(); + Instant currentTime = Instant.now().minusMillis(1); + NestedRecordWithUpdateBehavior nestedRecord = new NestedRecordWithUpdateBehavior(); + nestedRecord.setId("id167"); + nestedRecord.setNestedUpdateBehaviorAttribute(TEST_BEHAVIOUR_ATTRIBUTE); + RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); record.setId("id123"); record.setCreatedOn(INSTANT_1); record.setLastUpdatedOn(INSTANT_2); + record.setNestedRecord(nestedRecord); mappedTable.updateItem(record); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(record); @@ -81,28 +91,57 @@ public void updateBehaviors_firstUpdate() { assertThat(persistedRecord.getLastAutoUpdatedOnMillis().getEpochSecond()).isGreaterThanOrEqualTo(currentTime.getEpochSecond()); assertThat(persistedRecord.getCreatedAutoUpdateOn()).isAfterOrEqualTo(currentTime); + + assertThat(persistedRecord.getNestedRecord().getId()).isEqualTo("id167"); + assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isAfterOrEqualTo(currentTime); + assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isAfterOrEqualTo(currentTime); + assertThat(persistedRecord.getCreatedAutoUpdateOn()).isAfterOrEqualTo(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()); + assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isEqualTo(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()); } @Test public void updateBehaviors_secondUpdate() { - Instant beforeUpdateInstant = Instant.now(); + Instant beforeUpdateInstant = Instant.now().minusMillis(1); + + NestedRecordWithUpdateBehavior secondNestedRecord = new NestedRecordWithUpdateBehavior(); + secondNestedRecord.setId("id199"); + secondNestedRecord.setNestedUpdateBehaviorAttribute(TEST_BEHAVIOUR_ATTRIBUTE); + + NestedRecordWithUpdateBehavior nestedRecord = new NestedRecordWithUpdateBehavior(); + nestedRecord.setId("id155"); + nestedRecord.setNestedUpdateBehaviorAttribute(TEST_BEHAVIOUR_ATTRIBUTE); + nestedRecord.setNestedRecord(secondNestedRecord); + RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); record.setId("id123"); record.setCreatedOn(INSTANT_1); record.setLastUpdatedOn(INSTANT_2); + record.setNestedRecord(nestedRecord); mappedTable.updateItem(record); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(record); assertThat(persistedRecord.getVersion()).isEqualTo(1L); + Instant firstUpdatedTime = persistedRecord.getLastAutoUpdatedOn(); Instant createdAutoUpdateOn = persistedRecord.getCreatedAutoUpdateOn(); + assertThat(firstUpdatedTime).isAfterOrEqualTo(beforeUpdateInstant); assertThat(persistedRecord.getFormattedLastAutoUpdatedOn().getEpochSecond()) .isGreaterThanOrEqualTo(beforeUpdateInstant.getEpochSecond()); + assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isNotNull(); + assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()) + .isEqualTo(firstUpdatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()) + .isEqualTo(firstUpdatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedRecord().getNestedCreatedTimeAttribute()) + .isEqualTo(firstUpdatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedRecord().getNestedUpdatedTimeAttribute()) + .isEqualTo(firstUpdatedTime); record.setVersion(1L); record.setCreatedOn(INSTANT_2); record.setLastUpdatedOn(INSTANT_2); + record.setNestedRecord(nestedRecord); mappedTable.updateItem(record); persistedRecord = mappedTable.getItem(record); @@ -113,6 +152,14 @@ public void updateBehaviors_secondUpdate() { Instant secondUpdatedTime = persistedRecord.getLastAutoUpdatedOn(); assertThat(secondUpdatedTime).isAfterOrEqualTo(firstUpdatedTime); assertThat(persistedRecord.getCreatedAutoUpdateOn()).isEqualTo(createdAutoUpdateOn); + assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()) + .isEqualTo(secondUpdatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()) + .isEqualTo(secondUpdatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedRecord().getNestedCreatedTimeAttribute()) + .isEqualTo(secondUpdatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedRecord().getNestedUpdatedTimeAttribute()) + .isEqualTo(secondUpdatedTime); } @Test @@ -164,7 +211,7 @@ public void updateBehaviors_transactWriteItems_secondUpdate() { @Test public void when_updatingNestedObjectWithSingleLevel_existingInformationIsPreserved_scalar_only_update() { - + Instant currentTime = Instant.now().minusMillis(1); NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); @@ -173,26 +220,35 @@ public void when_updatingNestedObjectWithSingleLevel_existingInformationIsPreser mappedTable.putItem(record); + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + + Instant nestedCreatedTime = persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute(); + Instant nestedUpdatedTime = persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute(); + assertThat(nestedCreatedTime).isAfter(currentTime); + assertThat(nestedUpdatedTime).isEqualTo(nestedCreatedTime); + NestedRecordWithUpdateBehavior updatedNestedRecord = new NestedRecordWithUpdateBehavior(); long updatedNestedCounter = 10L; updatedNestedRecord.setNestedCounter(updatedNestedCounter); - RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); - update_record.setId("id123"); - update_record.setVersion(1L); - update_record.setNestedRecord(updatedNestedRecord); + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setNestedRecord(updatedNestedRecord); - mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, - TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1); + TEST_BEHAVIOUR_ATTRIBUTE, currentTime); + assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isEqualTo(nestedCreatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isAfter(nestedUpdatedTime); } @Test public void when_updatingNestedObjectWithSingleLevel_default_mode_update_newMapCreated() { - + Instant currentTime = Instant.now().minusMillis(1); NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); @@ -201,25 +257,34 @@ public void when_updatingNestedObjectWithSingleLevel_default_mode_update_newMapC mappedTable.putItem(record); + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + + Instant nestedCreatedTime = persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute(); + Instant nestedUpdatedTime = persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute(); + assertThat(nestedCreatedTime).isNotNull(); + assertThat(nestedUpdatedTime).isEqualTo(nestedCreatedTime); + NestedRecordWithUpdateBehavior updatedNestedRecord = new NestedRecordWithUpdateBehavior(); long updatedNestedCounter = 10L; updatedNestedRecord.setNestedCounter(updatedNestedCounter); - RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); - update_record.setId("id123"); - update_record.setVersion(1L); - update_record.setNestedRecord(updatedNestedRecord); + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setNestedRecord(updatedNestedRecord); - mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.DEFAULT)); + mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.DEFAULT)); - RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, null); + verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, currentTime); + assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isAfter(nestedCreatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isAfter(nestedUpdatedTime); } @Test public void when_updatingNestedObjectWithSingleLevel_with_no_mode_update_newMapCreated() { - + Instant currentTime = Instant.now().minusMillis(1); NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); @@ -232,16 +297,73 @@ public void when_updatingNestedObjectWithSingleLevel_with_no_mode_update_newMapC long updatedNestedCounter = 10L; updatedNestedRecord.setNestedCounter(updatedNestedCounter); - RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); - update_record.setId("id123"); - update_record.setVersion(1L); - update_record.setNestedRecord(updatedNestedRecord); + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setNestedRecord(updatedNestedRecord); - mappedTable.updateItem(r -> r.item(update_record)); + mappedTable.updateItem(r -> r.item(updateRecord)); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, null); + verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, currentTime); + } + + @Test + public void when_updatingNestedObjectList_no_matter_mode_update_newListCreated_with_timestampGenerated() { + Instant currentTime = Instant.now().minusMillis(1); + NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); + nestedRecord.setNestedUpdatedTimeAttribute(null); + NestedRecordListElement firstElement = new NestedRecordListElement(); + firstElement.setId("id1"); + firstElement.setAttribute("attr1"); + NestedRecordListElement secondElement = new NestedRecordListElement(); + secondElement.setId("id2"); + secondElement.setAttribute("attr2"); + nestedRecord.setNestedRecordList(ImmutableList.of(firstElement, secondElement)); + + RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); + record.setId("id123"); + record.setNestedRecord(nestedRecord); + record.setNestedRecordList(ImmutableList.of(firstElement, secondElement)); + + mappedTable.putItem(record); + + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + + List nestedRecordList = persistedRecord.getNestedRecord().getNestedRecordList(); + Instant firstOperationTime = nestedRecordList.get(0).getTimeAttributeElement(); + + assertThat(persistedRecord.getNestedRecordList().get(0).getTimeAttributeElement()).isAfter(currentTime); + assertThat(persistedRecord.getNestedRecordList().get(1).getTimeAttributeElement()).isAfter(currentTime); + assertThat(nestedRecordList.get(0).getTimeAttributeElement()).isAfter(currentTime); + assertThat(nestedRecordList.get(1).getTimeAttributeElement()).isEqualTo(firstOperationTime); + + NestedRecordWithUpdateBehavior updatedNestedRecord = new NestedRecordWithUpdateBehavior(); + long updatedNestedCounter = 10L; + updatedNestedRecord.setNestedUpdatedTimeAttribute(null); + firstElement.setAttribute("attr44"); + secondElement.setAttribute("attr55"); + updatedNestedRecord.setNestedCounter(updatedNestedCounter); + updatedNestedRecord.setNestedRecordList(ImmutableList.of(firstElement, secondElement)); + + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setNestedRecord(updatedNestedRecord); + updateRecord.setNestedRecordList(ImmutableList.of(firstElement)); + + mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + + nestedRecordList = persistedRecord.getNestedRecord().getNestedRecordList(); + + assertThat(persistedRecord.getNestedRecordList()).hasSize(1); + assertThat(persistedRecord.getNestedRecordList().get(0).getTimeAttributeElement()).isAfter(firstOperationTime); + assertThat(nestedRecordList).hasSize(2); + assertThat(nestedRecordList.get(0).getTimeAttributeElement()).isAfter(firstOperationTime); + assertThat(nestedRecordList.get(1).getTimeAttributeElement()).isAfter(firstOperationTime); } @Test @@ -258,15 +380,59 @@ public void when_updatingNestedObjectToEmptyWithSingleLevel_existingInformationI NestedRecordWithUpdateBehavior updatedNestedRecord = new NestedRecordWithUpdateBehavior(); - RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); - update_record.setId("id123"); - update_record.setVersion(1L); - update_record.setNestedRecord(updatedNestedRecord); + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setNestedRecord(updatedNestedRecord); + + mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + assertThat(persistedRecord.getNestedRecord()).isNotNull(); + assertThat(persistedRecord.getNestedRecord().getId()).isNull(); + assertThat(persistedRecord.getNestedRecord().getNestedCounter()).isNull(); + assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isNull(); + assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isNotNull(); + } + + @Test + public void when_updatingNestedObjectWithSingleLevel_updateBehaviorIsChecked_scalar_only_update() { + Instant currentTime = Instant.now().minusMillis(1); + NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); + + RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); + record.setId("id123"); + record.setNestedRecord(nestedRecord); - mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + mappedTable.putItem(record); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - assertThat(persistedRecord.getNestedRecord()).isNull(); + + Instant nestedCreatedTime = persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute(); + Instant nestedUpdatedTime = persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute(); + assertThat(nestedCreatedTime).isAfter(currentTime); + assertThat(nestedUpdatedTime).isEqualTo(nestedCreatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isEqualTo(TEST_BEHAVIOUR_ATTRIBUTE); + + NestedRecordWithUpdateBehavior updatedNestedRecord = new NestedRecordWithUpdateBehavior(); + long updatedNestedCounter = 10L; + updatedNestedRecord.setNestedCounter(updatedNestedCounter); + updatedNestedRecord.setNestedUpdateBehaviorAttribute(TEST_BEHAVIOUR_ATTRIBUTE + "updated"); + + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setNestedRecord(updatedNestedRecord); + + mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + + //WRITE_IF_NOT_EXISTS detected on createdTimeAttribute and updateBehaviorAttribute -> not changed + assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isEqualTo(nestedCreatedTime); + assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isEqualTo(TEST_BEHAVIOUR_ATTRIBUTE); + + assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isAfter(nestedUpdatedTime); } private NestedRecordWithUpdateBehavior createNestedWithDefaults(String id, Long counter) { @@ -274,7 +440,6 @@ private NestedRecordWithUpdateBehavior createNestedWithDefaults(String id, Long nestedRecordWithDefaults.setId(id); nestedRecordWithDefaults.setNestedCounter(counter); nestedRecordWithDefaults.setNestedUpdateBehaviorAttribute(TEST_BEHAVIOUR_ATTRIBUTE); - nestedRecordWithDefaults.setNestedTimeAttribute(INSTANT_1); return nestedRecordWithDefaults; } @@ -282,31 +447,34 @@ private NestedRecordWithUpdateBehavior createNestedWithDefaults(String id, Long private void verifyMultipleLevelNestingTargetedUpdateBehavior(NestedRecordWithUpdateBehavior nestedRecord, long updatedOuterNestedCounter, long updatedInnerNestedCounter, - String test_behav_attribute, - Instant expected_time) { + String testBehaviorAttribute, + Instant expectedTime) { assertThat(nestedRecord).isNotNull(); assertThat(nestedRecord.getNestedRecord()).isNotNull(); assertThat(nestedRecord.getNestedCounter()).isEqualTo(updatedOuterNestedCounter); + assertThat(nestedRecord.getNestedCreatedTimeAttribute()).isAfter(expectedTime); + assertThat(nestedRecord.getNestedUpdatedTimeAttribute()).isAfter(expectedTime); assertThat(nestedRecord.getNestedRecord()).isNotNull(); assertThat(nestedRecord.getNestedRecord().getNestedCounter()).isEqualTo(updatedInnerNestedCounter); assertThat(nestedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isEqualTo( - test_behav_attribute); - assertThat(nestedRecord.getNestedRecord().getNestedTimeAttribute()).isEqualTo(expected_time); + testBehaviorAttribute); + assertThat(nestedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isAfter(expectedTime); + assertThat(nestedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isAfter(expectedTime); } private void verifySingleLevelNestingTargetedUpdateBehavior(NestedRecordWithUpdateBehavior nestedRecord, - long updatedNestedCounter, String expected_behav_attr, - Instant expected_time) { + long updatedNestedCounter, String expectedBehaviorAttr, + Instant expectedTime) { assertThat(nestedRecord).isNotNull(); assertThat(nestedRecord.getNestedCounter()).isEqualTo(updatedNestedCounter); - assertThat(nestedRecord.getNestedUpdateBehaviorAttribute()).isEqualTo(expected_behav_attr); - assertThat(nestedRecord.getNestedTimeAttribute()).isEqualTo(expected_time); + assertThat(nestedRecord.getNestedUpdateBehaviorAttribute()).isEqualTo(expectedBehaviorAttr); + assertThat(nestedRecord.getNestedCreatedTimeAttribute()).isAfter(expectedTime); + assertThat(nestedRecord.getNestedUpdatedTimeAttribute()).isAfter(expectedTime); } @Test public void when_updatingNestedObjectWithMultipleLevels_inScalarOnlyMode_existingInformationIsPreserved() { - NestedRecordWithUpdateBehavior nestedRecord1 = createNestedWithDefaults("id789", 50L); NestedRecordWithUpdateBehavior nestedRecord2 = createNestedWithDefaults("id456", 0L); @@ -327,12 +495,12 @@ public void when_updatingNestedObjectWithMultipleLevels_inScalarOnlyMode_existin long outerNestedCounter = 200L; updatedNestedRecord1.setNestedCounter(outerNestedCounter); - RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); - update_record.setId("id123"); - update_record.setVersion(1L); - update_record.setNestedRecord(updatedNestedRecord1); + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setNestedRecord(updatedNestedRecord1); - mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); @@ -342,7 +510,6 @@ public void when_updatingNestedObjectWithMultipleLevels_inScalarOnlyMode_existin @Test public void when_updatingNestedObjectWithMultipleLevels_inMapsOnlyMode_existingInformationIsPreserved() { - NestedRecordWithUpdateBehavior nestedRecord1 = createNestedWithDefaults("id789", 50L); NestedRecordWithUpdateBehavior nestedRecord2 = createNestedWithDefaults("id456", 0L); @@ -358,12 +525,12 @@ public void when_updatingNestedObjectWithMultipleLevels_inMapsOnlyMode_existingI long outerNestedCounter = 200L; updatedNestedRecord1.setNestedCounter(outerNestedCounter); - RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); - update_record.setId("id123"); - update_record.setVersion(1L); - update_record.setNestedRecord(updatedNestedRecord1); + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setNestedRecord(updatedNestedRecord1); - mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); + mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); @@ -373,7 +540,7 @@ public void when_updatingNestedObjectWithMultipleLevels_inMapsOnlyMode_existingI @Test public void when_updatingNestedObjectWithMultipleLevels_default_mode_existingInformationIsErased() { - + Instant currentTime = Instant.now().minusMillis(1); NestedRecordWithUpdateBehavior nestedRecord1 = createNestedWithDefaults("id789", 50L); NestedRecordWithUpdateBehavior nestedRecord2 = createNestedWithDefaults("id456", 0L); @@ -394,22 +561,21 @@ public void when_updatingNestedObjectWithMultipleLevels_default_mode_existingInf long outerNestedCounter = 200L; updatedNestedRecord1.setNestedCounter(outerNestedCounter); - RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); - update_record.setId("id123"); - update_record.setVersion(1L); - update_record.setNestedRecord(updatedNestedRecord1); + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setNestedRecord(updatedNestedRecord1); - mappedTable.updateItem(r -> r.item(update_record)); + mappedTable.updateItem(r -> r.item(updateRecord)); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); verifyMultipleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), outerNestedCounter, innerNestedCounter, null, - null); + currentTime); } @Test public void when_updatingNestedNonScalarObject_scalar_only_update_throwsDynamoDBException() { - NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); nestedRecord.setAttribute(TEST_ATTRIBUTE); @@ -418,35 +584,34 @@ public void when_updatingNestedNonScalarObject_scalar_only_update_throwsDynamoDB mappedTable.putItem(record); - RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); - update_record.setId("id123"); - update_record.setVersion(1L); - update_record.setKey("abc"); - update_record.setNestedRecord(nestedRecord); + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setKey("abc"); + updateRecord.setNestedRecord(nestedRecord); - assertThatThrownBy(() -> mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY))) + assertThatThrownBy(() -> mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY))) .isInstanceOf(DynamoDbException.class); } @Test public void when_updatingNestedMap_mapsOnlyMode_newMapIsCreatedAndStored() { - RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); record.setId("id123"); mappedTable.putItem(record); - RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); - update_record.setId("id123"); - update_record.setVersion(1L); - update_record.setKey("abc"); + RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); + updateRecord.setId("id123"); + updateRecord.setVersion(1L); + updateRecord.setKey("abc"); NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); nestedRecord.setAttribute(TEST_ATTRIBUTE); - update_record.setNestedRecord(nestedRecord); + updateRecord.setNestedRecord(nestedRecord); RecordWithUpdateBehaviors persistedRecord = - mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); + mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), 5L, TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1); @@ -470,21 +635,20 @@ public void when_emptyNestedRecordIsSet_emptyMapIsStoredInTable() { .build()); assertThat(getItemResponse.item().get("nestedRecord")).isNotNull(); - assertThat(getItemResponse.item().get("nestedRecord").toString()).isEqualTo("AttributeValue(M={nestedTimeAttribute" - + "=AttributeValue(NUL=true), " - + "nestedRecord=AttributeValue(NUL=true), " - + "attribute=AttributeValue(NUL=true), " - + "id=AttributeValue(NUL=true), " - + "nestedUpdateBehaviorAttribute=AttributeValue" - + "(NUL=true), nestedCounter=AttributeValue" - + "(NUL=true), nestedVersionedAttribute" - + "=AttributeValue(NUL=true)})"); + Map nestedRecord = getItemResponse.item().get("nestedRecord").m(); + assertThat(nestedRecord.get("nestedCreatedTimeAttribute")).isNotNull(); + assertThat(nestedRecord.get("nestedUpdatedTimeAttribute")).isNotNull(); + assertTrue(nestedRecord.get("id").nul()); + assertTrue(nestedRecord.get("nestedRecord").nul()); + assertTrue(nestedRecord.get("attribute").nul()); + assertTrue(nestedRecord.get("nestedUpdateBehaviorAttribute").nul()); + assertTrue(nestedRecord.get("nestedCounter").nul()); + assertTrue(nestedRecord.get("nestedVersionedAttribute").nul()); } @Test public void when_updatingNestedObjectWithSingleLevelFlattened_existingInformationIsPreserved_scalar_only_update() { - NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id123", 10L); CompositeRecord compositeRecord = new CompositeRecord(); @@ -513,12 +677,9 @@ public void when_updatingNestedObjectWithSingleLevelFlattened_existingInformatio verifySingleLevelNestingTargetedUpdateBehavior(persistedFlattenedRecord.getCompositeRecord().getNestedRecord(), 100L, TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1); } - - @Test public void when_updatingNestedObjectWithMultipleLevelFlattened_existingInformationIsPreserved_scalar_only_update() { - NestedRecordWithUpdateBehavior outerNestedRecord = createNestedWithDefaults("id123", 10L); NestedRecordWithUpdateBehavior innerNestedRecord = createNestedWithDefaults("id456", 5L); outerNestedRecord.setNestedRecord(innerNestedRecord); @@ -555,10 +716,11 @@ public void when_updatingNestedObjectWithMultipleLevelFlattened_existingInformat 50L, TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1); assertThat(persistedFlattenedRecord.getCompositeRecord().getNestedRecord().getNestedCounter()).isEqualTo(100L); assertThat(persistedFlattenedRecord.getCompositeRecord().getNestedRecord().getNestedRecord().getNestedCounter()).isEqualTo(50L); + assertThat(persistedFlattenedRecord.getCompositeRecord().getNestedRecord().getNestedRecord().getNestedCreatedTimeAttribute()).isNotNull(); } /** - * Currently, nested records are not updated through extensions. + * Currently, nested records are not updated through extensions (only the timestamp). */ @Test public void updateBehaviors_nested() { @@ -579,6 +741,6 @@ public void updateBehaviors_nested() { assertThat(persistedRecord.getNestedRecord().getNestedVersionedAttribute()).isNull(); assertThat(persistedRecord.getNestedRecord().getNestedCounter()).isNull(); assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isNull(); - assertThat(persistedRecord.getNestedRecord().getNestedTimeAttribute()).isNull(); + assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isNotNull(); } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordListElement.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordListElement.java new file mode 100644 index 000000000000..6cf9450f349c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordListElement.java @@ -0,0 +1,54 @@ +/* + * 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.models; + +import java.time.Instant; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +@DynamoDbBean +public class NestedRecordListElement { + private String id; + private String attribute; + private Instant timeAttributeElement; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getAttribute() { + return attribute; + } + + public void setAttribute(String attribute) { + this.attribute = attribute; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTimeAttributeElement() { + return timeAttributeElement; + } + + public void setTimeAttributeElement(Instant timeAttributeElement) { + this.timeAttributeElement = timeAttributeElement; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java index 883a89813c1a..df2e92c57392 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java @@ -18,6 +18,7 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS; import java.time.Instant; +import java.util.List; import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAtomicCounter; import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; @@ -30,10 +31,12 @@ public class NestedRecordWithUpdateBehavior { private String id; private String nestedUpdateBehaviorAttribute; private Long nestedVersionedAttribute; - private Instant nestedTimeAttribute; + private Instant nestedCreatedTimeAttribute; + private Instant nestedUpdatedTimeAttribute; private Long nestedCounter; private NestedRecordWithUpdateBehavior nestedRecord; private String attribute; + private List nestedRecordList; @DynamoDbPartitionKey public String getId() { @@ -63,12 +66,22 @@ public void setNestedVersionedAttribute(Long nestedVersionedAttribute) { } @DynamoDbAutoGeneratedTimestampAttribute - public Instant getNestedTimeAttribute() { - return nestedTimeAttribute; + @DynamoDbUpdateBehavior(WRITE_IF_NOT_EXISTS) + public Instant getNestedCreatedTimeAttribute() { + return nestedCreatedTimeAttribute; } - public void setNestedTimeAttribute(Instant nestedTimeAttribute) { - this.nestedTimeAttribute = nestedTimeAttribute; + public void setNestedCreatedTimeAttribute(Instant nestedCreatedTimeAttribute) { + this.nestedCreatedTimeAttribute = nestedCreatedTimeAttribute; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getNestedUpdatedTimeAttribute() { + return nestedUpdatedTimeAttribute; + } + + public void setNestedUpdatedTimeAttribute(Instant nestedUpdatedTimeAttribute) { + this.nestedUpdatedTimeAttribute = nestedUpdatedTimeAttribute; } @DynamoDbAtomicCounter @@ -95,4 +108,10 @@ public String getAttribute() { public void setAttribute(String attribute) { this.attribute = attribute; } + + public List getNestedRecordList() { return nestedRecordList;} + + public void setNestedRecordList(List nestedRecordList) { + this.nestedRecordList = nestedRecordList; + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java index 8bd874fee002..dbc9a6695d8f 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; import java.time.Instant; +import java.util.List; import software.amazon.awssdk.enhanced.dynamodb.converters.EpochMillisFormatTestConverter; import software.amazon.awssdk.enhanced.dynamodb.converters.TimeFormatUpdateTestConverter; import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; @@ -40,6 +41,7 @@ public class RecordWithUpdateBehaviors { private Instant formattedLastAutoUpdatedOn; private NestedRecordWithUpdateBehavior nestedRecord; private String key; + private List nestedRecordList; @DynamoDbPartitionKey public String getId() { @@ -133,4 +135,10 @@ public NestedRecordWithUpdateBehavior getNestedRecord() { public void setNestedRecord(NestedRecordWithUpdateBehavior nestedRecord) { this.nestedRecord = nestedRecord; } + + public List getNestedRecordList() { return nestedRecordList;} + + public void setNestedRecordList(List nestedRecordList) { + this.nestedRecordList = nestedRecordList; + } }