From 778d12bdc8b5698e23553e806ed95811cfea90d7 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Sun, 11 Jan 2026 16:38:33 +0200 Subject: [PATCH 01/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- ...-AmazonDynamoDBEnhancedClient-96aff9e.json | 6 + ...AutoGeneratedTimestampRecordExtension.java | 108 +- .../internal/EnhancedClientUtils.java | 21 + .../extensions/utility/NestedRecordUtils.java | 140 + .../operations/UpdateItemOperation.java | 6 +- .../update/UpdateExpressionUtils.java | 41 +- .../annotations/DynamoDbUpdateBehavior.java | 5 + .../extensions/NestedRecordUtilsTest.java | 58 + .../AutoGeneratedTimestampRecordTest.java | 310 +- .../AutogeneratedTimestampTest.java | 595 +++ .../NestedUpdateBehaviorTest.java | 516 +++ .../functionaltests/UpdateBehaviorTest.java | 338 +- .../models/AutogeneratedTimestampModels.java | 3618 +++++++++++++++++ .../models/NestedRecordListElement.java | 54 + .../NestedRecordWithUpdateBehavior.java | 29 +- .../models/RecordWithUpdateBehaviors.java | 12 +- 16 files changed, 5647 insertions(+), 210 deletions(-) create mode 100644 .changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtils.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/NestedRecordUtilsTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutogeneratedTimestampTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampModels.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordListElement.java diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json new file mode 100644 index 000000000000..8ed0b2c84583 --- /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..27bf8065a015 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.internal.EnhancedClientUtils.getNestedSchema; +import static software.amazon.awssdk.enhanced.dynamodb.internal.extensions.utility.NestedRecordUtils.getTableSchemaForListElement; +import static software.amazon.awssdk.enhanced.dynamodb.internal.extensions.utility.NestedRecordUtils.reconstructCompositeKey; +import static software.amazon.awssdk.enhanced.dynamodb.internal.extensions.utility.NestedRecordUtils.resolveSchemasPerPath; + 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,103 @@ 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()) { + TableSchema listElementSchema = getTableSchemaForListElement(nestedSchema, nestedKey); + 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()); + } + }); + 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/internal/EnhancedClientUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java index 61d750e98a7e..3787e12a9a34 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 @@ -29,6 +29,7 @@ import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.OperationContext; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; @@ -204,4 +205,24 @@ 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. When the attribute is a + * parameterized type (e.g., List), it retrieves the schema of the first type parameter. Otherwise, it retrieves the schema + * directly from the attribute's enhanced type. + * + * @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) { + EnhancedType enhancedType = parentSchema.converterForAttribute(attributeName).type(); + List> rawClassParameters = enhancedType.rawClassParameters(); + + if (rawClassParameters != null && !rawClassParameters.isEmpty()) { + enhancedType = rawClassParameters.get(0); + } + + return enhancedType.tableSchema(); + } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtils.java new file mode 100644 index 000000000000..3bb378e2e6f1 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtils.java @@ -0,0 +1,140 @@ +/* + * 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.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)) { + Optional> staticSchema = getNestedSchema(rootSchema, key); + listElementSchema = + staticSchema.isPresent() + ? staticSchema.get() + : 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]; + Optional> nestedListSchema = getNestedSchema(currentSchema, attributeName); + listElementSchema = nestedListSchema + .orElseThrow(() -> new IllegalArgumentException("Unable to resolve schema for list element at: " + key)); + } + } 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/operations/UpdateItemOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java index 0ffe361b5aed..cd98db7417bc 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 @@ -132,7 +132,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 +275,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 +284,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..c666b2644a8b --- /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.internal.extensions.utility.NestedRecordUtils.getTableSchemaForListElement; +import static software.amazon.awssdk.enhanced.dynamodb.internal.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..e1f9835ec1e3 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,10 +15,14 @@ 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; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel2RecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel3RecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel4Record; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForNestedRecordWithList; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; @@ -27,11 +31,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 +42,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; @@ -70,17 +69,13 @@ public class AutoGeneratedTimestampRecordTest extends LocalDynamoDbSyncTestBase 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) .addAttribute(Instant.class, a -> a.name("generated") - .getter(FlattenedRecord::getGenerated) - .setter(FlattenedRecord::setGenerated) - .tags(autoGeneratedTimestampAttribute())) + .getter(FlattenedRecord::getGenerated) + .setter(FlattenedRecord::setGenerated) + .tags(autoGeneratedTimestampAttribute())) .build(); private static final TableSchema TABLE_SCHEMA = @@ -103,23 +98,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(NestedStaticRecordWithList.class, + buildStaticSchemaForNestedRecordWithList(), + 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 +155,86 @@ public void deleteTable() { } @Test - public void putNewRecordSetsInitialAutoGeneratedTimestamp() { - Record item = new Record().setId("id").setAttribute("one"); + public void putNewRecord_setsInitialTimestamps_onAllNestedBeanLevels() { + NestedStaticLevel4Record nestedLevel4 = new NestedStaticLevel4Record().setAttr("attrL4"); + NestedStaticLevel3RecordWithList nestedLevel3 = + new NestedStaticLevel3RecordWithList().setAttr("attrL3").setLevel4(nestedLevel4); + NestedStaticLevel2RecordWithList level2 = + new NestedStaticLevel2RecordWithList().setAttr("attrL2").setLevel3(nestedLevel3); + NestedStaticRecordWithList nestedLevel1 = new NestedStaticRecordWithList().setAttr("attrL1").setLevel2(level2); + + 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(); + + NestedStaticLevel4Record expL4 = new NestedStaticLevel4Record() + .setTime(MOCKED_INSTANT_NOW).setAttr("attrL4"); + NestedStaticLevel3RecordWithList expL3 = new NestedStaticLevel3RecordWithList() + .setTime(MOCKED_INSTANT_NOW).setAttr("attrL3").setLevel4(expL4); + NestedStaticLevel2RecordWithList expL2 = new NestedStaticLevel2RecordWithList() + .setTime(MOCKED_INSTANT_NOW).setAttr("attrL2").setLevel3(expL3); + NestedStaticRecordWithList expL1 = new NestedStaticRecordWithList() + .setTime(MOCKED_INSTANT_NOW).setAttr("attrL1").setLevel2(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("time").s(), is(MOCKED_INSTANT_NOW.toString())); + + // level2 assertions + Map lvl2Map = lvl1Map.get("level2").m(); + assertThat(lvl2Map.get("time").s(), is(MOCKED_INSTANT_NOW.toString())); + + // level3 assertions + Map lvl3Map = lvl2Map.get("level3").m(); + assertThat(lvl3Map.get("time").s(), is(MOCKED_INSTANT_NOW.toString())); + + // level4 assertions + Map lvl4Map = lvl3Map.get("level4").m(); + assertThat(lvl4Map.get("time").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 NestedStaticRecordWithList() + .setAttr("attribute")))); GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); FlattenedRecord flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_NOW); + NestedStaticRecordWithList expectednestedLevel1 = new NestedStaticRecordWithList().setTime(MOCKED_INSTANT_NOW) + .setAttr("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("time").s(), + is(MOCKED_INSTANT_NOW.toString())); } @Test @@ -226,7 +268,7 @@ public void putExistingRecordUpdatedWithAutoFormattedTimestamps() { .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_ONE) .setFlattenedRecord(flattenedRecord); - System.out.println("result "+result); + System.out.println("result " + result); assertThat(result, is(expectedRecord)); // The data in DDB is stored in converted time format assertThat(itemAsStoredInDDB.item().get("convertedLastUpdatedDate").s(), is("14 01 2019 14:00:00")); @@ -256,12 +298,12 @@ public void putItemFollowedByUpdates() { itemAsStoredInDDB = getItemAsStoredFromDDB(); flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_UPDATE_ONE); expectedRecord = new Record().setId("id") - .setAttribute("one") - .setLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setConvertedLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setCreatedDate(MOCKED_INSTANT_NOW) - .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_ONE) - .setFlattenedRecord(flattenedRecord); + .setAttribute("one") + .setLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) + .setConvertedLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) + .setCreatedDate(MOCKED_INSTANT_NOW) + .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_ONE) + .setFlattenedRecord(flattenedRecord); assertThat(result, is(expectedRecord)); // The data in DDB is stored in converted time format assertThat(itemAsStoredInDDB.item().get("convertedLastUpdatedDate").s(), is("14 01 2019 14:00:00")); @@ -350,12 +392,12 @@ public void updateExistingRecordWithConditionExpressions() { Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); FlattenedRecord flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_UPDATE_ONE); Record expectedRecord = new Record().setId("id") - .setAttribute("one") - .setLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setConvertedLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setCreatedDate(MOCKED_INSTANT_NOW) - .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_ONE) - .setFlattenedRecord(flattenedRecord); + .setAttribute("one") + .setLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) + .setConvertedLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) + .setCreatedDate(MOCKED_INSTANT_NOW) + .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_ONE) + .setFlattenedRecord(flattenedRecord); assertThat(result, is(expectedRecord)); } @@ -373,9 +415,9 @@ public void putItemConditionTestFailure() { thrown.expect(ConditionalCheckFailedException.class); mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) - .item(new Record().setId("id").setAttribute("one")) - .conditionExpression(conditionExpression) - .build()); + .item(new Record().setId("id").setAttribute("one")) + .conditionExpression(conditionExpression) + .build()); } @@ -396,7 +438,7 @@ public void updateItemConditionTestFailure() { } @Test - public void incorrectTypeForAutoUpdateTimestampThrowsException(){ + public void incorrectTypeForAutoUpdateTimestampThrowsException() { thrown.expect(IllegalArgumentException.class); thrown.expectMessage("Attribute 'lastUpdatedDate' of Class type class java.lang.String is not a suitable " @@ -415,6 +457,76 @@ public void incorrectTypeForAutoUpdateTimestampThrowsException(){ .build(); } + @Test + public void putItemFollowedByUpdatesShouldGenerateTimestampsOnNestedFields() { + mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one") + .setNestedRecord(new NestedStaticRecordWithList().setAttr("attribute")))); + mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").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 NestedStaticRecordWithList().setAttr( + "attribute1")))); + itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute1")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").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 NestedStaticRecordWithList().setAttr( + "attribute2")))); + itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute2")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").s(), + is(MOCKED_INSTANT_UPDATE_TWO.toString())); + } + + @Test + public void putItemFollowedByUpdatesShouldGenerateTimestampsOnNestedFieldsList() { + mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one") + .setNestedRecord(new NestedStaticRecordWithList().setAttr("attribute")))); + mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); + GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").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 NestedStaticRecordWithList().setAttr( + "attribute1")))); + itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute1")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").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 NestedStaticRecordWithList().setAttr( + "attribute2")))); + itemAsStoredInDDB = getItemAsStoredFromDDB(); + + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute2")); + assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").s(), + is(MOCKED_INSTANT_UPDATE_TWO.toString())); + } + private GetItemResponse getItemAsStoredFromDDB() { Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s("id").build()); @@ -424,6 +536,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 +581,7 @@ private static class Record { private Instant convertedLastUpdatedDate; private Instant lastUpdatedDateInEpochMillis; private FlattenedRecord flattenedRecord; + private NestedStaticRecordWithList nestedLevel1; private String getId() { return id; @@ -496,6 +646,15 @@ public Record setFlattenedRecord(FlattenedRecord flattenedRecord) { return this; } + public NestedStaticRecordWithList getNestedRecord() { + return nestedLevel1; + } + + public Record setNestedRecord(NestedStaticRecordWithList nestedLevel1) { + this.nestedLevel1 = nestedLevel1; + return this; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -511,13 +670,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 +690,7 @@ public String toString() { ", convertedLastUpdatedDate=" + convertedLastUpdatedDate + ", lastUpdatedDateInEpochMillis=" + lastUpdatedDateInEpochMillis + ", flattenedRecord=" + flattenedRecord + - '}'; - } - } - - 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 + + ", nestedRecord=" + nestedLevel1 + '}'; } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutogeneratedTimestampTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutogeneratedTimestampTest.java new file mode 100644 index 000000000000..1395c7e5bf51 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutogeneratedTimestampTest.java @@ -0,0 +1,595 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hamcrest.CoreMatchers.everyItem; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNotNull; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_CHILD1; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_CHILD2; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL1; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL2; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL3; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL4; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.CHILD1_KEY; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.CHILD2_KEY; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ID_1; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.LEVEL2_KEY; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableRecordWithMap; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleImmutableRecordWithMap; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.TIME_ATTR; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildBeanSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildBeanSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildImmutableSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildImmutableSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedBeanRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedBeanRecordWithMap; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedImmutableRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedImmutableRecordWithMap; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedStaticRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleBeanRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleBeanRecordWithMap; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleImmutableRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleImmutableRecordWithMap; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleStaticRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticImmutableSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticImmutableSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForSimpleRecordWithList; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; +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.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanLevel2RecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanLevel3RecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanLevel4Record; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanRecordWithMap; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableLevel2RecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableLevel3RecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableLevel4Record; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel2RecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel3RecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel4Record; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleBeanRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleBeanRecordWithMap; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleStaticRecordWithList; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; + +@RunWith(Parameterized.class) +public class AutogeneratedTimestampTest extends LocalDynamoDbSyncTestBase { + + // Test configuration constants + private static final String TIMESTAMP_STRING = "2019-01-13T14:00:00Z"; + private static final String INVALID_SCHEMA_ERROR = "Invalid table schema"; + + // Table name suffixes + private static final String BASE_TABLE_NAME = "autogen-timestamp-test"; + private static final String SIMPLE_BEAN_TABLE_SUFFIX = "-simple-bean-"; + private static final String NESTED_BEAN_TABLE_SUFFIX = "-nested-bean-"; + private static final String SIMPLE_IMMUTABLE_TABLE_SUFFIX = "-simple-immutable-"; + private static final String NESTED_IMMUTABLE_TABLE_SUFFIX = "-nested-immutable-"; + private static final String SIMPLE_STATIC_TABLE_SUFFIX = "-simple-static-"; + private static final String NESTED_STATIC_TABLE_SUFFIX = "-nested-static-"; + private static final String SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX = "-simple-static-immutable-"; + private static final String NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX = "-nested-static-immutable-"; + + private static final Clock mockClock = Mockito.mock(Clock.class); + private static final Instant MOCKED_INSTANT_NOW = + Instant.now(Clock.fixed(Instant.parse(TIMESTAMP_STRING), ZoneOffset.UTC)); + private DynamoDbEnhancedClient enhancedClient; + private String currentTestTableName; + + private enum RecordLevel {SIMPLE, NESTED} + + private enum SchemaType {BEAN, IMMUTABLE, STATIC, STATIC_IMMUTABLE} + + @Parameterized.Parameters(name = "{0}-{1}") + public static Collection data() { + return Arrays.stream(SchemaType.values()) + .flatMap(schema -> Arrays.stream(RecordLevel.values()) + .map(level -> new Object[] {schema, level})) + .collect(Collectors.toList()); + } + + @Parameterized.Parameter(0) + public SchemaType schemaType; + + @Parameterized.Parameter(1) + public RecordLevel recordLevel; + + @Before + public void beforeClass() { + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_NOW); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.builder() + .baseClock(mockClock) + .build()) + .build(); + } + + @After + public void deleteTable() { + try { + if (currentTestTableName != null) { + getDynamoDbClient().deleteTable(r -> r.tableName(currentTestTableName)); + } + } catch (ResourceNotFoundException e) { + // Table didn't get created, ignore. + } + } + + + @Test + public void shouldPopulateTimestamps_forRecordWithList() { + getTestCaseForRecordWithList(schemaType, recordLevel).run(); + } + + @Test + public void shouldThrowException_forRecordWithSet() { + if (schemaType == SchemaType.BEAN || schemaType == SchemaType.IMMUTABLE) { + assertThatThrownBy(() -> getTestCaseForRecordWithSet(schemaType, recordLevel).run()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Converter not found for EnhancedType(java.util.Set<"); + } else { + assertThatThrownBy(() -> getTestCaseForRecordWithSet(schemaType, recordLevel).run()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("SetAttributeConverter cannot be created with a parameterized type of '"); + } + } + + @Test + public void shouldThrowException_forRecordWithMap() { + if (schemaType == SchemaType.BEAN || schemaType == SchemaType.IMMUTABLE) { + assertThatThrownBy(() -> getTestCaseForRecordWithMap(schemaType, recordLevel).run()) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected: is <2019-01-13T14:00:00Z>\n but: was null"); + } else { + assertThatThrownBy(() -> getTestCaseForRecordWithMap(schemaType, recordLevel).run()) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Converter not found for EnhancedType(java.util.Map { + throw new IllegalArgumentException(INVALID_SCHEMA_ERROR); + }; + } + } + + private Runnable getTestCaseForRecordWithSet(SchemaType schema, RecordLevel level) { + switch (schema) { + case BEAN: + return (level == RecordLevel.SIMPLE) + ? AutogeneratedTimestampModels::buildBeanSchemaForSimpleRecordWithSet + : AutogeneratedTimestampModels::buildBeanSchemaForNestedRecordWithSet; + case IMMUTABLE: + return (level == RecordLevel.SIMPLE) + ? AutogeneratedTimestampModels::buildImmutableSchemaForSimpleRecordWithSet + : AutogeneratedTimestampModels::buildImmutableSchemaForNestedRecordWithSet; + case STATIC: + return (level == RecordLevel.SIMPLE) + ? AutogeneratedTimestampModels::buildStaticSchemaForSimpleRecordWithSet + : AutogeneratedTimestampModels::buildStaticSchemaForNestedRecordWithSet; + case STATIC_IMMUTABLE: + return (level == RecordLevel.SIMPLE) + ? AutogeneratedTimestampModels::buildStaticImmutableSchemaForSimpleRecordWithSet + : AutogeneratedTimestampModels::buildStaticImmutableSchemaForNestedRecordWithSet; + default: + return () -> { + throw new IllegalArgumentException(INVALID_SCHEMA_ERROR); + }; + } + } + + private Runnable getTestCaseForRecordWithMap(SchemaType schema, RecordLevel level) { + switch (schema) { + case BEAN: + return (level == RecordLevel.SIMPLE) + ? this::testAutogeneratedTimestamp_givenBeanSchema_onSimpleRecordWithMap_throwsException + : this::testAutogeneratedTimestamp_givenBeanSchema_onNestedRecordWithMap_throwsException; + case IMMUTABLE: + return (level == RecordLevel.SIMPLE) + ? this::testAutogeneratedTimestamp_givenImmutableSchema_onSimpleRecordWithMap_throwsException + : this::testAutogeneratedTimestamp_givenImmutableSchema_onNestedRecordWithMap_throwsException; + case STATIC: + return (level == RecordLevel.SIMPLE) + ? AutogeneratedTimestampModels::buildStaticSchemaForSimpleRecordWithMap + : AutogeneratedTimestampModels::buildStaticSchemaForNestedRecordWithMap; + case STATIC_IMMUTABLE: + return (level == RecordLevel.SIMPLE) + ? AutogeneratedTimestampModels::buildStaticImmutableSchemaForSimpleRecordWithMap + : AutogeneratedTimestampModels::buildStaticImmutableSchemaForNestedRecordWithMap; + default: + return () -> { + throw new IllegalArgumentException(INVALID_SCHEMA_ERROR); + }; + } + } + + + // Bean table schema + Record with List + private void testAutogeneratedTimestamp_givenBeanSchema_onSimpleRecordWithList_populatesTimestamps() { + TableSchema schema = buildBeanSchemaForSimpleRecordWithList(); + DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, + schema, + buildSimpleBeanRecordWithList()); + + SimpleBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(result.getAttr(), is(ATTR_LEVEL1)); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + + assertNotNull(result.getChildList()); + assertThat(result.getChildList().get(0).getAttr(), is(ATTR_CHILD1)); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(1).getAttr(), is(ATTR_CHILD2)); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + } + + private void testAutogeneratedTimestamp_givenBeanSchema_onNestedRecordWithList_populatesTimestamps() { + TableSchema schema = buildBeanSchemaForNestedRecordWithList(); + DynamoDbTable table = createAndPut(NESTED_BEAN_TABLE_SUFFIX, + schema, + buildNestedBeanRecordWithList()); + + NestedBeanRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(level1, notNullValue()); + assertThat(level1.getAttr(), is(ATTR_LEVEL1)); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2(), notNullValue()); + assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2List(), notNullValue()); + assertThat(level1.getLevel2List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedBeanLevel2RecordWithList level2 = level1.getLevel2(); + assertThat(level2, notNullValue()); + assertThat(level2.getAttr(), is(ATTR_LEVEL2)); + assertThat(level2.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level2.getLevel3(), notNullValue()); + assertThat(level2.getLevel3().getAttr(), is(ATTR_LEVEL3)); + assertThat(level2.getLevel3().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level2.getLevel3List(), notNullValue()); + assertThat(level2.getLevel3List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedBeanLevel3RecordWithList level3 = level2.getLevel3(); + assertThat(level3, notNullValue()); + assertThat(level3.getAttr(), is(ATTR_LEVEL3)); + assertThat(level3.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level3.getLevel4(), notNullValue()); + assertThat(level3.getLevel4().getAttr(), is(ATTR_LEVEL4)); + assertThat(level3.getLevel4().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level3.getLevel4List(), notNullValue()); + assertThat(level3.getLevel4List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedBeanLevel4Record level4 = level3.getLevel4(); + assertThat(level4, notNullValue()); + assertThat(level4.getAttr(), is(ATTR_LEVEL4)); + assertThat(level4.getTime(), is(MOCKED_INSTANT_NOW)); + } + + // Bean table schema + Record with Map + private void testAutogeneratedTimestamp_givenBeanSchema_onSimpleRecordWithMap_throwsException() { + TableSchema schema = AutogeneratedTimestampModels.buildBeanSchemaForSimpleRecordWithMap(); + DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, + schema, + buildSimpleBeanRecordWithMap()); + + SimpleBeanRecordWithMap result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(result.getAttr(), is(ATTR_LEVEL1)); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + + assertNotNull(result.getChildMap()); + assertThat(result.getChildMap().get(CHILD1_KEY).getAttr(), is(ATTR_CHILD1)); + assertThat(result.getChildMap().get(CHILD1_KEY).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildMap().get(CHILD2_KEY).getAttr(), is(ATTR_CHILD2)); + assertThat(result.getChildMap().get(CHILD2_KEY).getTime(), is(MOCKED_INSTANT_NOW)); + } + + private void testAutogeneratedTimestamp_givenBeanSchema_onNestedRecordWithMap_throwsException() { + TableSchema schema = AutogeneratedTimestampModels.buildBeanSchemaForNestedRecordWithMap(); + DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, + schema, + buildNestedBeanRecordWithMap()); + + NestedBeanRecordWithMap level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(level1, notNullValue()); + assertThat(level1.getAttr(), is(ATTR_LEVEL1)); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2(), notNullValue()); + assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + assertNotNull(level1.getLevel2Map()); + assertThat(level1.getLevel2Map().get(LEVEL2_KEY).getAttr(), is(ATTR_LEVEL2)); + assertThat(level1.getLevel2Map().get(LEVEL2_KEY).getTime(), is(MOCKED_INSTANT_NOW)); + } + + // Immutable table schema + Record with Map + private void testAutogeneratedTimestamp_givenImmutableSchema_onSimpleRecordWithMap_throwsException() { + TableSchema schema = + AutogeneratedTimestampModels.buildImmutableSchemaForSimpleRecordWithMap(); + DynamoDbTable table = createAndPut(SIMPLE_IMMUTABLE_TABLE_SUFFIX, + schema, + buildSimpleImmutableRecordWithMap()); + + SimpleImmutableRecordWithMap result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(result.getAttr(), is(ATTR_LEVEL1)); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + + assertNotNull(result.getChildMap()); + assertThat(result.getChildMap().get(CHILD1_KEY).getAttr(), is(ATTR_CHILD1)); + assertThat(result.getChildMap().get(CHILD1_KEY).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildMap().get(CHILD2_KEY).getAttr(), is(ATTR_CHILD2)); + assertThat(result.getChildMap().get(CHILD2_KEY).getTime(), is(MOCKED_INSTANT_NOW)); + } + + private void testAutogeneratedTimestamp_givenImmutableSchema_onNestedRecordWithMap_throwsException() { + TableSchema schema = + AutogeneratedTimestampModels.buildImmutableSchemaForNestedRecordWithMap(); + DynamoDbTable table = createAndPut(NESTED_IMMUTABLE_TABLE_SUFFIX, + schema, + buildNestedImmutableRecordWithMap()); + + NestedImmutableRecordWithMap level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(level1, notNullValue()); + assertThat(level1.getAttr(), is(ATTR_LEVEL1)); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2(), notNullValue()); + assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + assertNotNull(level1.getLevel2Map()); + assertThat(level1.getLevel2Map().get(LEVEL2_KEY).getAttr(), is(ATTR_LEVEL2)); + assertThat(level1.getLevel2Map().get(LEVEL2_KEY).getTime(), is(MOCKED_INSTANT_NOW)); + } + + // Immutable table schema + Record with List + private void testAutogeneratedTimestamp_givenImmutableSchema_onSimpleRecordWithList_populatesTimestamps() { + TableSchema schema = buildImmutableSchemaForSimpleRecordWithList(); + DynamoDbTable table = createAndPut(SIMPLE_IMMUTABLE_TABLE_SUFFIX, + schema, + buildSimpleImmutableRecordWithList()); + + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(result.getAttr(), is(ATTR_LEVEL1)); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + + assertNotNull(result.getChildList()); + assertThat(result.getChildList().get(0).getAttr(), is(ATTR_CHILD1)); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(1).getAttr(), is(ATTR_CHILD2)); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + } + + private void testAutogeneratedTimestamp_givenImmutableSchema_onNestedRecordWithList_populatesTimestamps() { + TableSchema schema = buildImmutableSchemaForNestedRecordWithList(); + DynamoDbTable table = createAndPut(NESTED_IMMUTABLE_TABLE_SUFFIX, + schema, + buildNestedImmutableRecordWithList()); + + NestedImmutableRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(level1, notNullValue()); + assertThat(level1.getAttr(), is(ATTR_LEVEL1)); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2(), notNullValue()); + assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2List(), notNullValue()); + assertThat(level1.getLevel2List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedImmutableLevel2RecordWithList level2 = level1.getLevel2(); + assertThat(level2, notNullValue()); + assertThat(level2.getAttr(), is(ATTR_LEVEL2)); + assertThat(level2.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level2.getLevel3(), notNullValue()); + assertThat(level2.getLevel3().getAttr(), is(ATTR_LEVEL3)); + assertThat(level2.getLevel3().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level2.getLevel3List(), notNullValue()); + assertThat(level2.getLevel3List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedImmutableLevel3RecordWithList level3 = level2.getLevel3(); + assertThat(level3, notNullValue()); + assertThat(level3.getAttr(), is(ATTR_LEVEL3)); + assertThat(level3.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level3.getLevel4(), notNullValue()); + assertThat(level3.getLevel4().getAttr(), is(ATTR_LEVEL4)); + assertThat(level3.getLevel4().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level3.getLevel4List(), notNullValue()); + assertThat(level3.getLevel4List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedImmutableLevel4Record level4 = level3.getLevel4(); + assertThat(level4, notNullValue()); + assertThat(level4.getAttr(), is(ATTR_LEVEL4)); + assertThat(level4.getTime(), is(MOCKED_INSTANT_NOW)); + } + + // Static table schema + Record with List + private void testAutogeneratedTimestamp_givenStaticSchema_onSimpleRecordWithList_populatesTimestamps() { + DynamoDbTable table = createAndPut(SIMPLE_STATIC_TABLE_SUFFIX, + buildStaticSchemaForSimpleRecordWithList(), + buildSimpleStaticRecordWithList()); + + SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(result.getAttr(), is(ATTR_LEVEL1)); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + + assertNotNull(result.getChildList()); + assertThat(result.getChildList().get(0).getAttr(), is(ATTR_CHILD1)); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(1).getAttr(), is(ATTR_CHILD2)); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + } + + private void testAutogeneratedTimestamp_givenStaticImmutableSchema_onSimpleRecordWithList_populatesTimestamps() { + DynamoDbTable table = createAndPut(SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX, + buildStaticImmutableSchemaForSimpleRecordWithList(), + buildSimpleImmutableRecordWithList()); + + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(result.getAttr(), is(ATTR_LEVEL1)); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + + assertNotNull(result.getChildList()); + assertThat(result.getChildList().get(0).getAttr(), is(ATTR_CHILD1)); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(1).getAttr(), is(ATTR_CHILD2)); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + } + + private void testAutogeneratedTimestamp_givenStaticSchema_onNestedRecordWithList_populatesTimestamps() { + DynamoDbTable table = createAndPut(NESTED_STATIC_TABLE_SUFFIX, + buildStaticSchemaForNestedRecordWithList(), + buildNestedStaticRecordWithList()); + + NestedStaticRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(level1, notNullValue()); + assertThat(level1.getAttr(), is(ATTR_LEVEL1)); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2(), notNullValue()); + assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2List(), notNullValue()); + assertThat(level1.getLevel2List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedStaticLevel2RecordWithList level2 = level1.getLevel2(); + assertThat(level2, notNullValue()); + assertThat(level2.getAttr(), is(ATTR_LEVEL2)); + assertThat(level2.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level2.getLevel3(), notNullValue()); + assertThat(level2.getLevel3().getAttr(), is(ATTR_LEVEL3)); + assertThat(level2.getLevel3().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level2.getLevel3List(), notNullValue()); + assertThat(level2.getLevel3List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedStaticLevel3RecordWithList level3 = level2.getLevel3(); + assertThat(level3, notNullValue()); + assertThat(level3.getAttr(), is(ATTR_LEVEL3)); + assertThat(level3.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level3.getLevel4(), notNullValue()); + assertThat(level3.getLevel4().getAttr(), is(ATTR_LEVEL4)); + assertThat(level3.getLevel4().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level3.getLevel4List(), notNullValue()); + assertThat(level3.getLevel4List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedStaticLevel4Record level4 = level3.getLevel4(); + assertThat(level4, notNullValue()); + assertThat(level4.getAttr(), is(ATTR_LEVEL4)); + assertThat(level4.getTime(), is(MOCKED_INSTANT_NOW)); + } + + private void testAutogeneratedTimestamp_givenStaticImmutableSchema_onNestedRecordWithList_populatesTimestamps() { + DynamoDbTable table = createAndPut(NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX, + buildStaticImmutableSchemaForNestedRecordWithList(), + buildNestedImmutableRecordWithList()); + + NestedImmutableRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + + assertThat(level1, notNullValue()); + assertThat(level1.getAttr(), is(ATTR_LEVEL1)); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2(), notNullValue()); + assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2List(), notNullValue()); + assertThat(level1.getLevel2List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedImmutableLevel2RecordWithList level2 = level1.getLevel2(); + assertThat(level2, notNullValue()); + assertThat(level2.getAttr(), is(ATTR_LEVEL2)); + assertThat(level2.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level2.getLevel3(), notNullValue()); + assertThat(level2.getLevel3().getAttr(), is(ATTR_LEVEL3)); + assertThat(level2.getLevel3().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level2.getLevel3List(), notNullValue()); + assertThat(level2.getLevel3List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedImmutableLevel3RecordWithList level3 = level2.getLevel3(); + assertThat(level3, notNullValue()); + assertThat(level3.getAttr(), is(ATTR_LEVEL3)); + assertThat(level3.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level3.getLevel4(), notNullValue()); + assertThat(level3.getLevel4().getAttr(), is(ATTR_LEVEL4)); + assertThat(level3.getLevel4().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level3.getLevel4List(), notNullValue()); + assertThat(level3.getLevel4List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); + + NestedImmutableLevel4Record level4 = level3.getLevel4(); + assertThat(level4, notNullValue()); + assertThat(level4.getAttr(), is(ATTR_LEVEL4)); + assertThat(level4.getTime(), is(MOCKED_INSTANT_NOW)); + } + + // Helper for table creation + item insert + private DynamoDbTable createAndPut(String tableSuffix, TableSchema schema, T item) { + currentTestTableName = BASE_TABLE_NAME + tableSuffix + System.nanoTime(); + DynamoDbTable table = enhancedClient.table(currentTestTableName, schema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem(item); + return table; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java new file mode 100644 index 000000000000..17cccd6ccae0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java @@ -0,0 +1,516 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL1; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ID_1; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildBeanSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildBeanSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildImmutableSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildImmutableSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedBeanRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedImmutableRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedStaticRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleBeanRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleImmutableRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleStaticRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticImmutableSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticImmutableSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForSimpleRecordWithList; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; +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.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleBeanRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.model.IgnoreNullsMode; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; + +@RunWith(Parameterized.class) +public class NestedUpdateBehaviorTest extends LocalDynamoDbSyncTestBase { + + private static final String TIMESTAMP_STRING = "2019-01-13T14:00:00Z"; + private static final String BASE_TABLE_NAME = "update-behavior-test"; + private static final String SIMPLE_BEAN_TABLE_SUFFIX = "-simple-bean-"; + private static final String NESTED_BEAN_TABLE_SUFFIX = "-nested-bean-"; + private static final String SIMPLE_IMMUTABLE_TABLE_SUFFIX = "-simple-immutable-"; + private static final String NESTED_IMMUTABLE_TABLE_SUFFIX = "-nested-immutable-"; + private static final String SIMPLE_STATIC_TABLE_SUFFIX = "-simple-static-"; + private static final String NESTED_STATIC_TABLE_SUFFIX = "-nested-static-"; + private static final String SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX = "-simple-static-immutable-"; + private static final String NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX = "-nested-static-immutable-"; + + private static final Clock mockClock = Mockito.mock(Clock.class); + private static final Instant MOCKED_INSTANT_NOW = + Instant.now(Clock.fixed(Instant.parse(TIMESTAMP_STRING), ZoneOffset.UTC)); + private DynamoDbEnhancedClient enhancedClient; + private String currentTestTableName; + + private enum RecordLevel {SIMPLE, NESTED} + + private enum SchemaType {BEAN, IMMUTABLE, STATIC, STATIC_IMMUTABLE} + + @Parameterized.Parameters(name = "{0}-{1}") + public static Collection data() { + return Arrays.stream(SchemaType.values()) + .flatMap(schema -> Arrays.stream(RecordLevel.values()) + .map(level -> new Object[] {schema, level})) + .collect(Collectors.toList()); + } + + @Parameterized.Parameter(0) + public SchemaType schemaType; + + @Parameterized.Parameter(1) + public RecordLevel recordLevel; + + @Before + public void beforeClass() { + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_NOW); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.builder() + .baseClock(mockClock) + .build()) + .build(); + } + + @After + public void deleteTable() { + try { + if (currentTestTableName != null) { + getDynamoDbClient().deleteTable(r -> r.tableName(currentTestTableName)); + } + } catch (ResourceNotFoundException e) { + // Table didn't get created, ignore. + } + } + + @Test + public void updateBehavior_writeIfNotExists_isRespectedOnNestedObjects() { + switch (schemaType) { + case BEAN: + if (recordLevel == RecordLevel.SIMPLE) { + testSimpleBeanUpdateBehavior(); + } else { + testNestedBeanUpdateBehavior(); + } + break; + case IMMUTABLE: + if (recordLevel == RecordLevel.SIMPLE) { + testSimpleImmutableUpdateBehavior(); + } else { + testNestedImmutableUpdateBehavior(); + } + break; + case STATIC: + if (recordLevel == RecordLevel.SIMPLE) { + testSimpleStaticUpdateBehavior(); + } else { + testNestedStaticUpdateBehavior(); + } + break; + case STATIC_IMMUTABLE: + if (recordLevel == RecordLevel.SIMPLE) { + testSimpleStaticImmutableUpdateBehavior(); + } else { + testNestedStaticImmutableUpdateBehavior(); + } + break; + } + } + + private void testSimpleBeanUpdateBehavior() { + TableSchema schema = buildBeanSchemaForSimpleRecordWithList(); + DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, + schema, + buildSimpleBeanRecordWithList()); + + SimpleBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + assertThat(result.getTime()).isEqualTo(MOCKED_INSTANT_NOW); + assertThat(result.getChildList()).hasSize(2); + + // update with new attr value - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) + result.setAttr("updated_level1"); + table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + SimpleBeanRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + assertThat(updated.getTime()).isNotNull().isEqualTo(MOCKED_INSTANT_NOW); // timestamp should update + } + + private void testNestedBeanUpdateBehavior() { + TableSchema schema = buildBeanSchemaForNestedRecordWithList(); + DynamoDbTable table = createAndPut(NESTED_BEAN_TABLE_SUFFIX, + schema, + buildNestedBeanRecordWithList()); + + NestedBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + assertThat(result.getLevel2()).isNotNull(); + assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); + assertThat(result.getLevel2().getLevel3()).isNotNull(); + assertThat(result.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); + assertThat(result.getLevel2().getLevel3().getLevel4()).isNotNull(); + assertThat(result.getLevel2().getLevel3().getLevel4().getAttr()).isEqualTo("attr_level4"); + + // update with new attr values - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) + result.setAttr("updated_level1"); + result.getLevel2().setAttr("updated_level2"); + result.getLevel2().getLevel3().setAttr("updated_level3"); + result.getLevel2().getLevel3().getLevel4().setAttr("updated_level4"); + table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + NestedBeanRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + assertThat(updated.getLevel2().getAttr()).isEqualTo("attr_level2"); // should NOT change + assertThat(updated.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); // should NOT change + assertThat(updated.getLevel2().getLevel3().getLevel4().getAttr()).isEqualTo("attr_level4"); // should NOT change + } + + private void testSimpleImmutableUpdateBehavior() { + TableSchema schema = buildImmutableSchemaForSimpleRecordWithList(); + DynamoDbTable table = createAndPut(SIMPLE_IMMUTABLE_TABLE_SUFFIX, + schema, + buildSimpleImmutableRecordWithList()); + + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + assertThat(result.getTime()).isEqualTo(MOCKED_INSTANT_NOW); + assertThat(result.getChildList()).hasSize(2); + + // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) + SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() + .id(ID_1).attr("updated_level1") + .build(); + table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + } + + private void testNestedImmutableUpdateBehavior() { + TableSchema schema = buildImmutableSchemaForNestedRecordWithList(); + DynamoDbTable table = createAndPut(NESTED_IMMUTABLE_TABLE_SUFFIX, + schema, + buildNestedImmutableRecordWithList()); + + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + assertThat(result.getLevel2()).isNotNull(); + assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); + assertThat(result.getLevel2().getLevel3()).isNotNull(); + assertThat(result.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); + + // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) + NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() + .id(ID_1).attr("updated_level1") + .build(); + table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + } + + private void testSimpleStaticUpdateBehavior() { + DynamoDbTable table = createAndPut(SIMPLE_STATIC_TABLE_SUFFIX, + buildStaticSchemaForSimpleRecordWithList(), + buildSimpleStaticRecordWithList()); + + SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + assertThat(result.getTime()).isEqualTo(MOCKED_INSTANT_NOW); + + // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) + result.setAttr("updated_level1"); + table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + SimpleStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + } + + private void testNestedStaticUpdateBehavior() { + DynamoDbTable table = createAndPut(NESTED_STATIC_TABLE_SUFFIX, + buildStaticSchemaForNestedRecordWithList(), + buildNestedStaticRecordWithList()); + + NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + assertThat(result.getLevel2()).isNotNull(); + assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); + assertThat(result.getLevel2().getLevel3()).isNotNull(); + assertThat(result.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); + + // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) + result.setAttr("updated_level1"); + result.getLevel2().setAttr("updated_level2"); + result.getLevel2().getLevel3().setAttr("updated_level3"); + table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + NestedStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + assertThat(updated.getLevel2().getAttr()).isEqualTo("attr_level2"); // should NOT change + assertThat(updated.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); // should NOT change + } + + private void testSimpleStaticImmutableUpdateBehavior() { + DynamoDbTable table = createAndPut(SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX, + buildStaticImmutableSchemaForSimpleRecordWithList(), + buildSimpleImmutableRecordWithList()); + + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + assertThat(result.getTime()).isEqualTo(MOCKED_INSTANT_NOW); + + // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) + SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() + .id(ID_1).attr("updated_level1") + .build(); + table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + } + + private void testNestedStaticImmutableUpdateBehavior() { + DynamoDbTable table = createAndPut(NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX, + buildStaticImmutableSchemaForNestedRecordWithList(), + buildNestedImmutableRecordWithList()); + + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + assertThat(result.getLevel2()).isNotNull(); + assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); + + // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) + NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() + .id(ID_1).attr("updated_level1") + .build(); + table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + + NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + } + + @Test + public void updateBehavior_writeAlways_updatesValuesOnNestedObjects() { + // WRITE_ALWAYS is the default behavior, so we test that id field (without annotation) updates correctly + switch (schemaType) { + case BEAN: + if (recordLevel == RecordLevel.SIMPLE) { + testSimpleBeanWriteAlways(); + } else { + testNestedBeanWriteAlways(); + } + break; + case IMMUTABLE: + if (recordLevel == RecordLevel.SIMPLE) { + testSimpleImmutableWriteAlways(); + } else { + testNestedImmutableWriteAlways(); + } + break; + case STATIC: + if (recordLevel == RecordLevel.SIMPLE) { + testSimpleStaticWriteAlways(); + } else { + testNestedStaticWriteAlways(); + } + break; + case STATIC_IMMUTABLE: + if (recordLevel == RecordLevel.SIMPLE) { + testSimpleStaticImmutableWriteAlways(); + } else { + testNestedStaticImmutableWriteAlways(); + } + break; + } + } + + private void testSimpleBeanWriteAlways() { + TableSchema schema = buildBeanSchemaForSimpleRecordWithList(); + SimpleBeanRecordWithList initial = buildSimpleBeanRecordWithList(); + initial.setId("initial_id"); + DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, schema, initial); + + SimpleBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("initial_id"))); + assertThat(result.getId()).isEqualTo("initial_id"); + + // update id (no annotation, defaults to WRITE_ALWAYS) - should change + result.setId("updated_id"); + table.updateItem(result); + + SimpleBeanRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + assertThat(updated).isNotNull(); + assertThat(updated.getId()).isEqualTo("updated_id"); + } + + private void testNestedBeanWriteAlways() { + TableSchema schema = buildBeanSchemaForNestedRecordWithList(); + NestedBeanRecordWithList initial = buildNestedBeanRecordWithList(); + initial.setId("initial_id"); + DynamoDbTable table = createAndPut(NESTED_BEAN_TABLE_SUFFIX, schema, initial); + + NestedBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("initial_id"))); + assertThat(result.getId()).isEqualTo("initial_id"); + + // update id - should change (WRITE_ALWAYS is default) + result.setId("updated_id"); + table.updateItem(result); + + NestedBeanRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + assertThat(updated).isNotNull(); + assertThat(updated.getId()).isEqualTo("updated_id"); + } + + private void testSimpleImmutableWriteAlways() { + TableSchema schema = buildImmutableSchemaForSimpleRecordWithList(); + SimpleImmutableRecordWithList initial = buildSimpleImmutableRecordWithList(); + DynamoDbTable table = createAndPut(SIMPLE_IMMUTABLE_TABLE_SUFFIX, schema, initial); + + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getId()).isEqualTo(ID_1); + + // update with new id - should change (WRITE_ALWAYS is default) + SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() + .id("updated_id").attr(ATTR_LEVEL1) + .build(); + table.updateItem(updated); + + SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + assertThat(afterUpdate).isNotNull(); + assertThat(afterUpdate.getId()).isEqualTo("updated_id"); + } + + private void testNestedImmutableWriteAlways() { + TableSchema schema = buildImmutableSchemaForNestedRecordWithList(); + NestedImmutableRecordWithList initial = buildNestedImmutableRecordWithList(); + DynamoDbTable table = createAndPut(NESTED_IMMUTABLE_TABLE_SUFFIX, schema, initial); + + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getId()).isEqualTo(ID_1); + + // update with new id - should change (WRITE_ALWAYS is default) + NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() + .id("updated_id").attr(ATTR_LEVEL1) + .build(); + table.updateItem(updated); + + NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + assertThat(afterUpdate).isNotNull(); + assertThat(afterUpdate.getId()).isEqualTo("updated_id"); + } + + private void testSimpleStaticWriteAlways() { + DynamoDbTable table = createAndPut(SIMPLE_STATIC_TABLE_SUFFIX, + buildStaticSchemaForSimpleRecordWithList(), + buildSimpleStaticRecordWithList()); + + SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getId()).isEqualTo(ID_1); + + // update id - should change (WRITE_ALWAYS is default) + result.setId("updated_id"); + table.updateItem(result); + + SimpleStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + assertThat(updated).isNotNull(); + assertThat(updated.getId()).isEqualTo("updated_id"); + } + + private void testNestedStaticWriteAlways() { + DynamoDbTable table = createAndPut(NESTED_STATIC_TABLE_SUFFIX, + buildStaticSchemaForNestedRecordWithList(), + buildNestedStaticRecordWithList()); + + NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getId()).isEqualTo(ID_1); + + // update id - should change (WRITE_ALWAYS is default) + result.setId("updated_id"); + table.updateItem(result); + + NestedStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + assertThat(updated).isNotNull(); + assertThat(updated.getId()).isEqualTo("updated_id"); + } + + private void testSimpleStaticImmutableWriteAlways() { + DynamoDbTable table = createAndPut(SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX, + buildStaticImmutableSchemaForSimpleRecordWithList(), + buildSimpleImmutableRecordWithList()); + + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getId()).isEqualTo(ID_1); + + // update with new id - should change (WRITE_ALWAYS is default) + SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() + .id("updated_id").attr(ATTR_LEVEL1) + .build(); + table.updateItem(updated); + + SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + assertThat(afterUpdate).isNotNull(); + assertThat(afterUpdate.getId()).isEqualTo("updated_id"); + } + + private void testNestedStaticImmutableWriteAlways() { + DynamoDbTable table = createAndPut(NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX, + buildStaticImmutableSchemaForNestedRecordWithList(), + buildNestedImmutableRecordWithList()); + + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + assertThat(result.getId()).isEqualTo(ID_1); + + // update with new id - should change (WRITE_ALWAYS is default) + NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() + .id("updated_id").attr(ATTR_LEVEL1) + .build(); + table.updateItem(updated); + + NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + assertThat(afterUpdate).isNotNull(); + assertThat(afterUpdate.getId()).isEqualTo("updated_id"); + } + + private DynamoDbTable createAndPut(String tableSuffix, TableSchema schema, T item) { + currentTestTableName = BASE_TABLE_NAME + tableSuffix + System.nanoTime(); + DynamoDbTable table = enhancedClient.table(currentTestTableName, schema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem(item); + return table; + } +} 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/AutogeneratedTimestampModels.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampModels.java new file mode 100644 index 000000000000..a164036211f3 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampModels.java @@ -0,0 +1,3618 @@ +/* + * 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 static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; +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.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; +import software.amazon.awssdk.utils.ImmutableMap; + +public final class AutogeneratedTimestampModels { + + private AutogeneratedTimestampModels() { + } + + // Constants + public static final String ID_1 = "1"; + public static final String ID_2 = "2"; + public static final String ID_ATTR = "id"; + public static final String TIME_ATTR = "time"; + + public static final String ATTR_LEVEL1 = "attr_level1"; + public static final String ATTR_LEVEL2 = "attr_level2"; + public static final String ATTR_LEVEL3 = "attr_level3"; + public static final String ATTR_LEVEL4 = "attr_level4"; + public static final String ATTR_CHILD1 = "attr_child1"; + public static final String ATTR_CHILD2 = "attr_child2"; + + public static final String CHILD1_KEY = "child1"; + public static final String CHILD2_KEY = "child2"; + public static final String LEVEL2_KEY = "level2"; + public static final String LEVEL3_KEY = "level3"; + public static final String LEVEL4_KEY = "level4"; + + + // Simple Bean Records + @DynamoDbBean + public static class SimpleBeanRecordWithList { + private String id; + private String attr; + private Instant time; + private List childList; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanRecordWithList setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public SimpleBeanRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public SimpleBeanRecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + + public List getChildList() { + return childList == null ? null : Collections.unmodifiableList(childList); + } + + public SimpleBeanRecordWithList setChildList(List childList) { + this.childList = childList; + return this; + } + } + + @DynamoDbBean + public static class SimpleBeanRecordWithSet { + private String id; + private String attr; + private Instant time; + private Set childSet; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanRecordWithSet setId(String id) { + this.id = id; + return this; + } + + public String getAttr() { + return attr; + } + + public SimpleBeanRecordWithSet setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public SimpleBeanRecordWithSet setTime(Instant time) { + this.time = time; + return this; + } + + public Set getChildSet() { + return childSet == null ? null : Collections.unmodifiableSet(childSet); + } + + public SimpleBeanRecordWithSet setChildSet(Set childSet) { + this.childSet = childSet; + return this; + } + } + + @DynamoDbBean + public static class SimpleBeanRecordWithMap { + private String id; + private String attr; + private Instant time; + private Map childMap; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanRecordWithMap setId(String id) { + this.id = id; + return this; + } + + public String getAttr() { + return attr; + } + + public SimpleBeanRecordWithMap setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public SimpleBeanRecordWithMap setTime(Instant time) { + this.time = time; + return this; + } + + public Map getChildMap() { + return childMap == null ? null : Collections.unmodifiableMap(childMap); + } + + public SimpleBeanRecordWithMap setChildMap(Map childMap) { + this.childMap = childMap; + return this; + } + } + + // simple record used by list/set/map as the deepest nested level + @DynamoDbBean + public static class SimpleBeanChild { + private String id; + private String attr; + private Instant time; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanChild setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public SimpleBeanChild setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public SimpleBeanChild setTime(Instant time) { + this.time = time; + return this; + } + } + + + // Nested Bean Records + @DynamoDbBean + public static class NestedBeanRecordWithList { + private String id; + private String attr; + private Instant time; + private NestedBeanLevel2RecordWithList level2; + private List level2List; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public NestedBeanRecordWithList setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public NestedBeanRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanRecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + public NestedBeanLevel2RecordWithList getLevel2() { + return level2; + } + + public NestedBeanRecordWithList setLevel2(NestedBeanLevel2RecordWithList level2) { + this.level2 = level2; + return this; + } + + public List getLevel2List() { + return level2List == null ? null : Collections.unmodifiableList(level2List); + } + + public NestedBeanRecordWithList setLevel2List(List level2List) { + this.level2List = level2List; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedBeanRecordWithList)) { + return false; + } + + NestedBeanRecordWithList that = (NestedBeanRecordWithList) o; + return Objects.equals(id, that.id) && + Objects.equals(attr, that.attr) && + Objects.equals(time, that.time) && + Objects.equals(level2, that.level2) && + Objects.equals(level2List, that.level2List); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level2); + result = 31 * result + Objects.hashCode(level2List); + return result; + } + } + + @DynamoDbBean + public static class NestedBeanLevel2RecordWithList { + private String attr; + private Instant time; + private NestedBeanLevel3RecordWithList level3; + private List level3List; + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public NestedBeanLevel2RecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanLevel2RecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + public NestedBeanLevel3RecordWithList getLevel3() { + return level3; + } + + public NestedBeanLevel2RecordWithList setLevel3(NestedBeanLevel3RecordWithList level3) { + this.level3 = level3; + return this; + } + + public List getLevel3List() { + return level3List; + } + + public NestedBeanLevel2RecordWithList setLevel3List(List level3List) { + this.level3List = level3List; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedBeanLevel2RecordWithList)) { + return false; + } + + NestedBeanLevel2RecordWithList that = (NestedBeanLevel2RecordWithList) o; + return Objects.equals(attr, that.attr) && + Objects.equals(time, that.time) && + Objects.equals(level3, that.level3) && + Objects.equals(level3List, that.level3List); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level3); + result = 31 * result + Objects.hashCode(level3List); + return result; + } + } + + @DynamoDbBean + public static class NestedBeanLevel3RecordWithList { + private String attr; + private Instant time; + private NestedBeanLevel4Record level4; + private List level4List; + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public NestedBeanLevel3RecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanLevel3RecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + public NestedBeanLevel4Record getLevel4() { + return level4; + } + + public NestedBeanLevel3RecordWithList setLevel4(NestedBeanLevel4Record level4) { + this.level4 = level4; + return this; + } + + public List getLevel4List() { + return level4List; + } + + public NestedBeanLevel3RecordWithList setLevel4List(List level4List) { + this.level4List = level4List; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedBeanLevel3RecordWithList)) { + return false; + } + + NestedBeanLevel3RecordWithList that = (NestedBeanLevel3RecordWithList) o; + return Objects.equals(attr, that.attr) && + Objects.equals(time, that.time) && + Objects.equals(level4, that.level4) && + Objects.equals(level4List, that.level4List); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level4); + result = 31 * result + Objects.hashCode(level4List); + return result; + } + } + + @DynamoDbBean + public static class NestedBeanRecordWithSet { + private String id; + private String attr; + private Instant time; + private NestedBeanLevel2RecordWithSet level2; + private Set level2Set; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public NestedBeanRecordWithSet setId(String v) { + id = v; + return this; + } + + public String getAttr() { + return attr; + } + + public NestedBeanRecordWithSet setAttr(String v) { + attr = v; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanRecordWithSet setTime(Instant v) { + time = v; + return this; + } + + public NestedBeanLevel2RecordWithSet getLevel2() { + return level2; + } + + public NestedBeanRecordWithSet setLevel2(NestedBeanLevel2RecordWithSet v) { + level2 = v; + return this; + } + + public Set getLevel2Set() { + return level2Set; + } + + public NestedBeanRecordWithSet setLevel2Set(Set v) { + level2Set = v; + return this; + } + } + + @DynamoDbBean + public static class NestedBeanLevel2RecordWithSet { + private String attr; + private Instant time; + private NestedBeanLevel3RecordWithSet level3; + private Set level3Set; + + public String getAttr() { + return attr; + } + + public NestedBeanLevel2RecordWithSet setAttr(String v) { + attr = v; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanLevel2RecordWithSet setTime(Instant v) { + time = v; + return this; + } + + public NestedBeanLevel3RecordWithSet getLevel3() { + return level3; + } + + public NestedBeanLevel2RecordWithSet setLevel3(NestedBeanLevel3RecordWithSet v) { + level3 = v; + return this; + } + + public Set getLevel3Set() { + return level3Set; + } + + public NestedBeanLevel2RecordWithSet setLevel3Set(Set v) { + level3Set = v; + return this; + } + } + + @DynamoDbBean + public static class NestedBeanLevel3RecordWithSet { + private String attr; + private Instant time; + private NestedBeanLevel4Record level4; + private Set level4Set; + + public String getAttr() { + return attr; + } + + public NestedBeanLevel3RecordWithSet setAttr(String v) { + attr = v; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanLevel3RecordWithSet setTime(Instant v) { + time = v; + return this; + } + + public NestedBeanLevel4Record getLevel4() { + return level4; + } + + public NestedBeanLevel3RecordWithSet setLevel4(NestedBeanLevel4Record v) { + level4 = v; + return this; + } + + public Set getLevel4Set() { + return level4Set; + } + + public NestedBeanLevel3RecordWithSet setLevel4Set(Set v) { + level4Set = v; + return this; + } + } + + @DynamoDbBean + public static class NestedBeanRecordWithMap { + private String id; + private String attr; + private Instant time; + private NestedBeanLevel2RecordWithMap level2; + private Map level2Map; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public NestedBeanRecordWithMap setId(String id) { + this.id = id; + return this; + } + + public String getAttr() { + return attr; + } + + public NestedBeanRecordWithMap setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanRecordWithMap setTime(Instant time) { + this.time = time; + return this; + } + + public NestedBeanLevel2RecordWithMap getLevel2() { + return level2; + } + + public NestedBeanRecordWithMap setLevel2(NestedBeanLevel2RecordWithMap level2) { + this.level2 = level2; + return this; + } + + public Map getLevel2Map() { + return Collections.unmodifiableMap(level2Map); + } + + public NestedBeanRecordWithMap setLevel2Map(Map level2Map) { + this.level2Map = level2Map; + return this; + } + } + + @DynamoDbBean + public static class NestedBeanLevel2RecordWithMap { + private String attr; + private Instant time; + private NestedBeanLevel3RecordWithMap level3; + private Map level3Map; + + public String getAttr() { + return attr; + } + + public NestedBeanLevel2RecordWithMap setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanLevel2RecordWithMap setTime(Instant time) { + this.time = time; + return this; + } + + public NestedBeanLevel3RecordWithMap getLevel3() { + return level3; + } + + public NestedBeanLevel2RecordWithMap setLevel3(NestedBeanLevel3RecordWithMap level3) { + this.level3 = level3; + return this; + } + + public Map getLevel3Map() { + return Collections.unmodifiableMap(level3Map); + } + + public NestedBeanLevel2RecordWithMap setLevel3Map(Map level3Map) { + this.level3Map = level3Map; + return this; + } + } + + @DynamoDbBean + public static class NestedBeanLevel3RecordWithMap { + private String attr; + private Instant time; + private NestedBeanLevel4Record level4; + private Map level4Map; + + public String getAttr() { + return attr; + } + + public NestedBeanLevel3RecordWithMap setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanLevel3RecordWithMap setTime(Instant time) { + this.time = time; + return this; + } + + public NestedBeanLevel4Record getLevel4() { + return level4; + } + + public NestedBeanLevel3RecordWithMap setLevel4(NestedBeanLevel4Record level4) { + this.level4 = level4; + return this; + } + + public Map getLevel4Map() { + return Collections.unmodifiableMap(level4Map); + } + + public NestedBeanLevel3RecordWithMap setLevel4Map(Map level4Map) { + this.level4Map = level4Map; + return this; + } + } + + // nested record used by list/set/map as the deepest nested level + @DynamoDbBean + public static class NestedBeanLevel4Record { + private String id; + private String attr; + private Instant time; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public NestedBeanLevel4Record setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public NestedBeanLevel4Record setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanLevel4Record setTime(Instant time) { + this.time = time; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedBeanLevel4Record)) { + return false; + } + + NestedBeanLevel4Record that = (NestedBeanLevel4Record) o; + return Objects.equals(attr, that.attr) && + Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + return result; + } + } + + + // Simple Immutable Records + @DynamoDbImmutable(builder = SimpleImmutableRecordWithList.Builder.class) + public static final class SimpleImmutableRecordWithList { + private final String id; + private final String attr; + private final Instant time; + private final List childList; + + private SimpleImmutableRecordWithList(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + this.childList = b.childList; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + + public List getChildList() { + return childList == null ? null : Collections.unmodifiableList(childList); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private List childList; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder childList(List childList) { + this.childList = childList; + return this; + } + + public SimpleImmutableRecordWithList build() { + return new SimpleImmutableRecordWithList(this); + } + } + } + + @DynamoDbImmutable(builder = SimpleImmutableRecordWithSet.Builder.class) + public static class SimpleImmutableRecordWithSet { + private final String id; + private final String attr; + private final Instant time; + private final Set childSet; + + private SimpleImmutableRecordWithSet(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + this.childSet = b.childSet; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + + public Set getChildSet() { + return Collections.unmodifiableSet(childSet); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private Set childSet; + + public Builder id(String v) { + this.id = v; + return this; + } + + public Builder attr(String v) { + this.attr = v; + return this; + } + + public Builder time(Instant v) { + this.time = v; + return this; + } + + public Builder childSet(Set v) { + this.childSet = v; + return this; + } + + public SimpleImmutableRecordWithSet build() { + return new SimpleImmutableRecordWithSet(this); + } + } + } + + @DynamoDbImmutable(builder = SimpleImmutableRecordWithMap.Builder.class) + public static class SimpleImmutableRecordWithMap { + private final String id; + private final String attr; + private final Instant time; + private final Map childMap; + + private SimpleImmutableRecordWithMap(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + this.childMap = b.childMap; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public Map getChildMap() { + return Collections.unmodifiableMap(childMap); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private Map childMap; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder childMap(Map childMap) { + this.childMap = childMap; + return this; + } + + public SimpleImmutableRecordWithMap build() { + return new SimpleImmutableRecordWithMap(this); + } + } + } + + // simple record used by list/set/map as the deepest nested level + @DynamoDbImmutable(builder = SimpleImmutableChild.Builder.class) + public static final class SimpleImmutableChild { + private final String id; + private final String attr; + private final Instant time; + + private SimpleImmutableChild(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public SimpleImmutableChild build() { + return new SimpleImmutableChild(this); + } + } + } + + + // Nested Immutable Records + @DynamoDbImmutable(builder = NestedImmutableRecordWithList.Builder.class) + public static final class NestedImmutableRecordWithList { + private final String id; + private final String attr; + private final Instant time; + private final NestedImmutableLevel2RecordWithList level2; + private final List level2List; + + private NestedImmutableRecordWithList(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + this.level2 = b.level2; + this.level2List = b.level2List; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableLevel2RecordWithList getLevel2() { + return level2; + } + + public List getLevel2List() { + return level2List == null ? null : Collections.unmodifiableList(level2List); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private NestedImmutableLevel2RecordWithList level2; + private List level2List; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder level2(NestedImmutableLevel2RecordWithList level2) { + this.level2 = level2; + return this; + } + + public Builder level2List(List level2List) { + this.level2List = level2List; + return this; + } + + public NestedImmutableRecordWithList build() { + return new NestedImmutableRecordWithList(this); + } + } + } + + @DynamoDbImmutable(builder = NestedImmutableLevel2RecordWithList.Builder.class) + public static final class NestedImmutableLevel2RecordWithList { + private final String attr; + private final Instant time; + private final NestedImmutableLevel3RecordWithList level3; + private final List level3List; + + private NestedImmutableLevel2RecordWithList(Builder b) { + this.attr = b.attr; + this.time = b.time; + this.level3 = b.level3; + this.level3List = b.level3List; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableLevel3RecordWithList getLevel3() { + return level3; + } + + public List getLevel3List() { + return level3List; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String attr; + private Instant time; + private NestedImmutableLevel3RecordWithList level3; + private List level3List; + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder level3(NestedImmutableLevel3RecordWithList level3) { + this.level3 = level3; + return this; + } + + public Builder level3List(List level3List) { + this.level3List = level3List; + return this; + } + + public NestedImmutableLevel2RecordWithList build() { + return new NestedImmutableLevel2RecordWithList(this); + } + } + } + + @DynamoDbImmutable(builder = NestedImmutableLevel3RecordWithList.Builder.class) + public static final class NestedImmutableLevel3RecordWithList { + private final String attr; + private final Instant time; + private final NestedImmutableLevel4Record level4; + private final List level4List; + + private NestedImmutableLevel3RecordWithList(Builder b) { + this.attr = b.attr; + this.time = b.time; + this.level4 = b.level4; + this.level4List = b.level4List; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableLevel4Record getLevel4() { + return level4; + } + + public List getLevel4List() { + return level4List; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String attr; + private Instant time; + private NestedImmutableLevel4Record level4; + private List level4List; + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder level4(NestedImmutableLevel4Record level4) { + this.level4 = level4; + return this; + } + + public Builder level4List(List level4List) { + this.level4List = level4List; + return this; + } + + public NestedImmutableLevel3RecordWithList build() { + return new NestedImmutableLevel3RecordWithList(this); + } + } + } + + @DynamoDbImmutable(builder = NestedImmutableRecordWithSet.Builder.class) + public static final class NestedImmutableRecordWithSet { + private final String id; + private final String attr; + private final Instant time; + private final NestedImmutableLevel2RecordWithSet level2; + private final Set level2Set; + + private NestedImmutableRecordWithSet(Builder b) { + id = b.id; + attr = b.attr; + time = b.time; + level2 = b.level2; + level2Set = b.level2Set; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableLevel2RecordWithSet getLevel2() { + return level2; + } + + public Set getLevel2Set() { + return level2Set == null ? null : Collections.unmodifiableSet(level2Set); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private NestedImmutableLevel2RecordWithSet level2; + private Set level2Set; + + public Builder id(String v) { + id = v; + return this; + } + + public Builder attr(String v) { + attr = v; + return this; + } + + public Builder time(Instant v) { + time = v; + return this; + } + + public Builder level2(NestedImmutableLevel2RecordWithSet v) { + level2 = v; + return this; + } + + public Builder level2Set(Set v) { + level2Set = v; + return this; + } + + public NestedImmutableRecordWithSet build() { + return new NestedImmutableRecordWithSet(this); + } + } + } + + @DynamoDbImmutable(builder = NestedImmutableLevel2RecordWithSet.Builder.class) + public static class NestedImmutableLevel2RecordWithSet { + private final String attr; + private final Instant time; + private final NestedImmutableLevel3RecordWithSet level3; + private final Set level3Set; + + private NestedImmutableLevel2RecordWithSet(Builder b) { + attr = b.attr; + time = b.time; + level3 = b.level3; + level3Set = b.level3Set; + } + + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableLevel3RecordWithSet getLevel3() { + return level3; + } + + public Set getLevel3Set() { + return level3Set == null ? null : Collections.unmodifiableSet(level3Set); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String attr; + private Instant time; + private NestedImmutableLevel3RecordWithSet level3; + private Set level3Set; + + public Builder attr(String v) { + attr = v; + return this; + } + + public Builder time(Instant v) { + time = v; + return this; + } + + public Builder level3(NestedImmutableLevel3RecordWithSet v) { + level3 = v; + return this; + } + + public Builder level3Set(Set v) { + level3Set = v; + return this; + } + + public NestedImmutableLevel2RecordWithSet build() { + return new NestedImmutableLevel2RecordWithSet(this); + } + } + } + + @DynamoDbImmutable(builder = NestedImmutableLevel3RecordWithSet.Builder.class) + public static class NestedImmutableLevel3RecordWithSet { + private final String attr; + private final Instant time; + private final NestedImmutableLevel4Record level4; + private final Set level4Set; + + private NestedImmutableLevel3RecordWithSet(Builder b) { + attr = b.attr; + time = b.time; + level4 = b.level4; + level4Set = b.level4Set; + } + + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableLevel4Record getLevel4() { + return level4; + } + + public Set getLevel4Set() { + return level4Set == null ? null : Collections.unmodifiableSet(level4Set); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String attr; + private Instant time; + private NestedImmutableLevel4Record level4; + private Set level4Set; + + public Builder attr(String v) { + attr = v; + return this; + } + + public Builder time(Instant v) { + time = v; + return this; + } + + public Builder level4(NestedImmutableLevel4Record v) { + level4 = v; + return this; + } + + public Builder level4Set(Set v) { + level4Set = v; + return this; + } + + public NestedImmutableLevel3RecordWithSet build() { + return new NestedImmutableLevel3RecordWithSet(this); + } + } + } + + @DynamoDbImmutable(builder = NestedImmutableRecordWithMap.Builder.class) + public static class NestedImmutableRecordWithMap { + private final String id; + private final String attr; + private final Instant time; + private final NestedImmutableLevel2RecordWithMap level2; + private final Map level2Map; + + private NestedImmutableRecordWithMap(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + this.level2 = b.level2; + this.level2Map = b.level2Map; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableLevel2RecordWithMap getLevel2() { + return level2; + } + + public Map getLevel2Map() { + return level2Map == null ? null : Collections.unmodifiableMap(level2Map); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private NestedImmutableLevel2RecordWithMap level2; + private Map level2Map; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder level2(NestedImmutableLevel2RecordWithMap v) { + this.level2 = v; + return this; + } + + public Builder level2Map(Map v) { + this.level2Map = v; + return this; + } + + public NestedImmutableRecordWithMap build() { + return new NestedImmutableRecordWithMap(this); + } + } + } + + @DynamoDbImmutable(builder = NestedImmutableLevel2RecordWithMap.Builder.class) + public static class NestedImmutableLevel2RecordWithMap { + private final String attr; + private final Instant time; + private final NestedImmutableLevel3RecordWithMap level3; + private final Map level3Map; + + private NestedImmutableLevel2RecordWithMap(Builder b) { + this.attr = b.attr; + this.time = b.time; + this.level3 = b.level3; + this.level3Map = b.level3Map; + } + + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableLevel3RecordWithMap getLevel3() { + return level3; + } + + public Map getLevel3Map() { + return level3Map == null ? null : Collections.unmodifiableMap(level3Map); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String attr; + private Instant time; + private NestedImmutableLevel3RecordWithMap level3; + private Map level3Map; + + public Builder attr(String v) { + this.attr = v; + return this; + } + + public Builder time(Instant v) { + this.time = v; + return this; + } + + public Builder level3(NestedImmutableLevel3RecordWithMap v) { + this.level3 = v; + return this; + } + + public Builder level3Map(Map v) { + this.level3Map = v; + return this; + } + + public NestedImmutableLevel2RecordWithMap build() { + return new NestedImmutableLevel2RecordWithMap(this); + } + } + } + + @DynamoDbImmutable(builder = NestedImmutableLevel3RecordWithMap.Builder.class) + public static class NestedImmutableLevel3RecordWithMap { + private final String attr; + private final Instant time; + private final NestedImmutableLevel4Record level4; + private final Map level4Map; + + private NestedImmutableLevel3RecordWithMap(Builder b) { + this.attr = b.attr; + this.time = b.time; + this.level4 = b.level4; + this.level4Map = b.level4Map; + } + + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableLevel4Record getLevel4() { + return level4; + } + + public Map getLevel4Map() { + return level4Map == null ? null : Collections.unmodifiableMap(level4Map); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String attr; + private Instant time; + private NestedImmutableLevel4Record level4; + private Map level4Map; + + public Builder attr(String v) { + this.attr = v; + return this; + } + + public Builder time(Instant v) { + this.time = v; + return this; + } + + public Builder level4(NestedImmutableLevel4Record v) { + this.level4 = v; + return this; + } + + public Builder level4Map(Map v) { + this.level4Map = v; + return this; + } + + public NestedImmutableLevel3RecordWithMap build() { + return new NestedImmutableLevel3RecordWithMap(this); + } + } + } + + // nested record used by list/set/map as the deepest nested level + @DynamoDbImmutable(builder = NestedImmutableLevel4Record.Builder.class) + public static final class NestedImmutableLevel4Record { + private final String id; + private final String attr; + private final Instant time; + + private NestedImmutableLevel4Record(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public NestedImmutableLevel4Record build() { + return new NestedImmutableLevel4Record(this); + } + } + } + + + // Simple Static Records + public static class SimpleStaticRecordWithSet { + private String id; + private String attr; + private Instant time; + private Set childSet; + + public String getId() { + return id; + } + + public SimpleStaticRecordWithSet setId(String id) { + this.id = id; + return this; + } + + public String getAttr() { + return attr; + } + + public SimpleStaticRecordWithSet setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public SimpleStaticRecordWithSet setTime(Instant time) { + this.time = time; + return this; + } + + public Set getChildSet() { + return Collections.unmodifiableSet(childSet); + } + + public SimpleStaticRecordWithSet setChildSet(Set childSet) { + this.childSet = childSet; + return this; + } + } + + public static class SimpleStaticRecordWithMap { + private String id; + private String attr; + private Instant time; + private Map childMap; + + public String getId() { + return id; + } + + public SimpleStaticRecordWithMap setId(String id) { + this.id = id; + return this; + } + + public String getAttr() { + return attr; + } + + public SimpleStaticRecordWithMap setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public SimpleStaticRecordWithMap setTime(Instant time) { + this.time = time; + return this; + } + + public Map getChildMap() { + return Collections.unmodifiableMap(childMap); + } + + public SimpleStaticRecordWithMap setChildMap(Map childMap) { + this.childMap = childMap; + return this; + } + } + + public static class SimpleStaticRecordWithList { + private String id; + private String attr; + private Instant time; + private List childList; + + public String getId() { + return id; + } + + public SimpleStaticRecordWithList setId(String id) { + this.id = id; + return this; + } + + + public String getAttr() { + return attr; + } + + public SimpleStaticRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public SimpleStaticRecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + + public List getChildList() { + return childList == null ? null : Collections.unmodifiableList(childList); + } + + public SimpleStaticRecordWithList setChildList(List childList) { + this.childList = childList; + return this; + } + } + + // simple record used by list/set/map as the deepest nested level + public static class SimpleStaticChild { + private String id; + private String attr; + private Instant time; + + public String getId() { + return id; + } + + public SimpleStaticChild setId(String id) { + this.id = id; + return this; + } + + + public String getAttr() { + return attr; + } + + public SimpleStaticChild setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public SimpleStaticChild setTime(Instant time) { + this.time = time; + return this; + } + } + + + // Nested Static Records + public static class NestedStaticRecordWithList { + private String id; + private String attr; + private Instant time; + private NestedStaticLevel2RecordWithList level2; + private List level2List; + + public String getId() { + return id; + } + + public NestedStaticRecordWithList setId(String id) { + this.id = id; + return this; + } + + + public String getAttr() { + return attr; + } + + public NestedStaticRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticRecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticLevel2RecordWithList getLevel2() { + return level2; + } + + public NestedStaticRecordWithList setLevel2(NestedStaticLevel2RecordWithList level2) { + this.level2 = level2; + return this; + } + + public List getLevel2List() { + return level2List; + } + + public NestedStaticRecordWithList setLevel2List(List level2List) { + this.level2List = level2List; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticRecordWithList)) { + return false; + } + + NestedStaticRecordWithList that = (NestedStaticRecordWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level2, that.level2) && Objects.equals(level2List, that.level2List); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level2); + result = 31 * result + Objects.hashCode(level2List); + return result; + } + } + + public static class NestedStaticLevel2RecordWithList { + private String attr; + private Instant time; + private NestedStaticLevel3RecordWithList level3; + private List level3List; + + + public String getAttr() { + return attr; + } + + public NestedStaticLevel2RecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticLevel2RecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticLevel3RecordWithList getLevel3() { + return level3; + } + + public NestedStaticLevel2RecordWithList setLevel3(NestedStaticLevel3RecordWithList level3) { + this.level3 = level3; + return this; + } + + public List getLevel3List() { + return level3List; + } + + public NestedStaticLevel2RecordWithList setLevel3List(List level3List) { + this.level3List = level3List; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticLevel2RecordWithList)) { + return false; + } + + NestedStaticLevel2RecordWithList that = (NestedStaticLevel2RecordWithList) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level3, that.level3) && Objects.equals(level3List, that.level3List); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level3); + result = 31 * result + Objects.hashCode(level3List); + return result; + } + } + + public static class NestedStaticLevel3RecordWithList { + private String attr; + private Instant time; + private NestedStaticLevel4Record level4; + private List level4List; + + + public String getAttr() { + return attr; + } + + public NestedStaticLevel3RecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticLevel3RecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticLevel4Record getLevel4() { + return level4; + } + + public NestedStaticLevel3RecordWithList setLevel4(NestedStaticLevel4Record level4) { + this.level4 = level4; + return this; + } + + public List getLevel4List() { + return level4List; + } + + public NestedStaticLevel3RecordWithList setLevel4List(List level4List) { + this.level4List = level4List; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticLevel3RecordWithList)) { + return false; + } + + NestedStaticLevel3RecordWithList that = (NestedStaticLevel3RecordWithList) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level4, that.level4) && Objects.equals(level4List, that.level4List); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level4); + result = 31 * result + Objects.hashCode(level4List); + return result; + } + } + + public static class NestedStaticRecordWithSet { + private String id; + private String attr; + private Instant time; + private NestedStaticLevel2RecordWithSet level2; + private Set level2List; + + public String getId() { + return id; + } + + public NestedStaticRecordWithSet setId(String id) { + this.id = id; + return this; + } + + + public String getAttr() { + return attr; + } + + public NestedStaticRecordWithSet setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticRecordWithSet setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticLevel2RecordWithSet getLevel2() { + return level2; + } + + public NestedStaticRecordWithSet setLevel2(NestedStaticLevel2RecordWithSet level2) { + this.level2 = level2; + return this; + } + + public Set getLevel2List() { + return level2List; + } + + public NestedStaticRecordWithSet setLevel2List(Set level2List) { + this.level2List = level2List; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticRecordWithSet)) { + return false; + } + + NestedStaticRecordWithSet that = (NestedStaticRecordWithSet) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level2, that.level2) && Objects.equals(level2List, that.level2List); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level2); + result = 31 * result + Objects.hashCode(level2List); + return result; + } + } + + public static class NestedStaticLevel2RecordWithSet { + private String attr; + private Instant time; + private NestedStaticLevel3RecordWithSet level3; + private Set level3Set; + + + public String getAttr() { + return attr; + } + + public NestedStaticLevel2RecordWithSet setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticLevel2RecordWithSet setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticLevel3RecordWithSet getLevel3() { + return level3; + } + + public NestedStaticLevel2RecordWithSet setLevel3(NestedStaticLevel3RecordWithSet level3) { + this.level3 = level3; + return this; + } + + public Set getLevel3Set() { + return level3Set; + } + + public NestedStaticLevel2RecordWithSet setLevel3Set(Set level3Set) { + this.level3Set = level3Set; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticLevel2RecordWithSet)) { + return false; + } + + NestedStaticLevel2RecordWithSet that = (NestedStaticLevel2RecordWithSet) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level3, that.level3) && Objects.equals(level3Set, that.level3Set); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level3); + result = 31 * result + Objects.hashCode(level3Set); + return result; + } + } + + public static class NestedStaticLevel3RecordWithSet { + private String attr; + private Instant time; + private NestedStaticLevel4Record level4; + private Set level4Set; + + + public String getAttr() { + return attr; + } + + public NestedStaticLevel3RecordWithSet setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticLevel3RecordWithSet setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticLevel4Record getLevel4() { + return level4; + } + + public NestedStaticLevel3RecordWithSet setLevel4(NestedStaticLevel4Record level4) { + this.level4 = level4; + return this; + } + + public Set getLevel4Set() { + return level4Set; + } + + public NestedStaticLevel3RecordWithSet setLevel4Set(Set level4Set) { + this.level4Set = level4Set; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticLevel3RecordWithSet)) { + return false; + } + + NestedStaticLevel3RecordWithSet that = (NestedStaticLevel3RecordWithSet) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level4, that.level4) && Objects.equals(level4Set, that.level4Set); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level4); + result = 31 * result + Objects.hashCode(level4Set); + return result; + } + } + + public static class NestedStaticRecordWithMap { + private String id; + private String attr; + private Instant time; + private NestedStaticLevel2RecordWithMap level2; + private Map level2Map; + + public String getId() { + return id; + } + + public NestedStaticRecordWithMap setId(String id) { + this.id = id; + return this; + } + + + public String getAttr() { + return attr; + } + + public NestedStaticRecordWithMap setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticRecordWithMap setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticLevel2RecordWithMap getLevel2() { + return level2; + } + + public NestedStaticRecordWithMap setLevel2(NestedStaticLevel2RecordWithMap level2) { + this.level2 = level2; + return this; + } + + public Map getLevel2Map() { + return level2Map; + } + + public NestedStaticRecordWithMap setLevel2Map(Map level2Map) { + this.level2Map = level2Map; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticRecordWithMap)) { + return false; + } + + NestedStaticRecordWithMap that = (NestedStaticRecordWithMap) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level2, that.level2) && Objects.equals(level2Map, that.level2Map); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level2); + result = 31 * result + Objects.hashCode(level2Map); + return result; + } + } + + public static class NestedStaticLevel2RecordWithMap { + private String attr; + private Instant time; + private NestedStaticLevel3RecordWithMap level3; + private Map level3Map; + + + public String getAttr() { + return attr; + } + + public NestedStaticLevel2RecordWithMap setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticLevel2RecordWithMap setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticLevel3RecordWithMap getLevel3() { + return level3; + } + + public NestedStaticLevel2RecordWithMap setLevel3(NestedStaticLevel3RecordWithMap level3) { + this.level3 = level3; + return this; + } + + public Map getLevel3Map() { + return level3Map; + } + + public NestedStaticLevel2RecordWithMap setLevel3Map(Map level3Map) { + this.level3Map = level3Map; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticLevel2RecordWithMap)) { + return false; + } + + NestedStaticLevel2RecordWithMap that = (NestedStaticLevel2RecordWithMap) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level3, that.level3) && Objects.equals(level3Map, that.level3Map); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level3); + result = 31 * result + Objects.hashCode(level3Map); + return result; + } + } + + public static class NestedStaticLevel3RecordWithMap { + private String attr; + private Instant time; + private NestedStaticLevel4Record level4; + private Map level4Map; + + public String getAttr() { + return attr; + } + + public NestedStaticLevel3RecordWithMap setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticLevel3RecordWithMap setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticLevel4Record getLevel4() { + return level4; + } + + public NestedStaticLevel3RecordWithMap setLevel4(NestedStaticLevel4Record level4) { + this.level4 = level4; + return this; + } + + public Map getLevel4Map() { + return level4Map; + } + + public NestedStaticLevel3RecordWithMap setLevel4Map(Map level4Map) { + this.level4Map = level4Map; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticLevel3RecordWithMap)) { + return false; + } + + NestedStaticLevel3RecordWithMap that = (NestedStaticLevel3RecordWithMap) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level4, that.level4) && Objects.equals(level4Map, that.level4Map); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + result = 31 * result + Objects.hashCode(level4); + result = 31 * result + Objects.hashCode(level4Map); + return result; + } + } + + // nested record used by list/set/map as the deepest nested level + public static class NestedStaticLevel4Record { + private String attr; + private Instant time; + + public String getAttr() { + return attr; + } + + public NestedStaticLevel4Record setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticLevel4Record setTime(Instant time) { + this.time = time; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NestedStaticLevel4Record)) { + return false; + } + + NestedStaticLevel4Record that = (NestedStaticLevel4Record) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(time); + return result; + } + } + + + // Bean Table Schemas for Simple Records + public static TableSchema buildBeanSchemaForSimpleRecordWithList() { + return BeanTableSchema.create(SimpleBeanRecordWithList.class); + } + + public static TableSchema buildBeanSchemaForSimpleRecordWithSet() { + return BeanTableSchema.create(SimpleBeanRecordWithSet.class); + } + + public static TableSchema buildBeanSchemaForSimpleRecordWithMap() { + return BeanTableSchema.create(SimpleBeanRecordWithMap.class); + } + + // Bean Table Schemas for Nested Records + public static TableSchema buildBeanSchemaForNestedRecordWithList() { + return BeanTableSchema.create(NestedBeanRecordWithList.class); + } + + public static TableSchema buildBeanSchemaForNestedRecordWithSet() { + return BeanTableSchema.create(NestedBeanRecordWithSet.class); + } + + public static TableSchema buildBeanSchemaForNestedRecordWithMap() { + return BeanTableSchema.create(NestedBeanRecordWithMap.class); + } + + + // Immutable Table Schemas for Simple Records + public static TableSchema buildImmutableSchemaForSimpleRecordWithList() { + return ImmutableTableSchema.create(SimpleImmutableRecordWithList.class); + } + + public static TableSchema buildImmutableSchemaForSimpleRecordWithSet() { + return ImmutableTableSchema.create(SimpleImmutableRecordWithSet.class); + } + + public static TableSchema buildImmutableSchemaForSimpleRecordWithMap() { + return ImmutableTableSchema.create(SimpleImmutableRecordWithMap.class); + } + + + // Immutable Table Schemas for Nested Records + public static TableSchema buildImmutableSchemaForNestedRecordWithList() { + return ImmutableTableSchema.create(NestedImmutableRecordWithList.class); + } + + public static TableSchema buildImmutableSchemaForNestedRecordWithSet() { + return ImmutableTableSchema.create(NestedImmutableRecordWithSet.class); + } + + public static TableSchema buildImmutableSchemaForNestedRecordWithMap() { + return ImmutableTableSchema.create(NestedImmutableRecordWithMap.class); + } + + + // Static Table Schemas for Simple Records + public static TableSchema buildStaticSchemaForSimpleRecordWithList() { + return StaticTableSchema.builder(SimpleStaticRecordWithList.class) + .newItemSupplier(SimpleStaticRecordWithList::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(SimpleStaticRecordWithList::getId) + .setter(SimpleStaticRecordWithList::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(SimpleStaticRecordWithList::getAttr) + .setter(SimpleStaticRecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(SimpleStaticRecordWithList::getTime) + .setter(SimpleStaticRecordWithList::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.listOf(EnhancedType.documentOf( + SimpleStaticChild.class, + buildStaticSchemaForSimpleChildRecord())), + a -> a.name("childList") + .getter(SimpleStaticRecordWithList::getChildList) + .setter(SimpleStaticRecordWithList::setChildList)) + .build(); + } + + public static StaticTableSchema buildStaticSchemaForSimpleRecordWithSet() { + return StaticTableSchema.builder(SimpleStaticRecordWithSet.class) + .newItemSupplier(SimpleStaticRecordWithSet::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(SimpleStaticRecordWithSet::getId) + .setter(SimpleStaticRecordWithSet::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(SimpleStaticRecordWithSet::getAttr) + .setter(SimpleStaticRecordWithSet::setAttr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(SimpleStaticRecordWithSet::getTime) + .setter(SimpleStaticRecordWithSet::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.setOf( + EnhancedType.documentOf( + SimpleStaticChild.class, + buildStaticSchemaForSimpleChildRecord())), + a -> a.name("childSet") + .getter(SimpleStaticRecordWithSet::getChildSet) + .setter(SimpleStaticRecordWithSet::setChildSet)) + .build(); + } + + public static TableSchema buildStaticSchemaForSimpleRecordWithMap() { + return StaticTableSchema.builder(SimpleStaticRecordWithMap.class) + .newItemSupplier(SimpleStaticRecordWithMap::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(SimpleStaticRecordWithMap::getId) + .setter(SimpleStaticRecordWithMap::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(SimpleStaticRecordWithMap::getAttr) + .setter(SimpleStaticRecordWithMap::setAttr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(SimpleStaticRecordWithMap::getTime) + .setter(SimpleStaticRecordWithMap::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.mapOf( + String.class, SimpleStaticChild.class), + a -> a.name("childMap") + .getter(SimpleStaticRecordWithMap::getChildMap) + .setter(SimpleStaticRecordWithMap::setChildMap)) + .build(); + } + + // schema of the simple record used by list/set/map as the deepest simple level + public static TableSchema buildStaticSchemaForSimpleChildRecord() { + return StaticTableSchema.builder(SimpleStaticChild.class) + .newItemSupplier(SimpleStaticChild::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(SimpleStaticChild::getId) + .setter(SimpleStaticChild::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(SimpleStaticChild::getAttr) + .setter(SimpleStaticChild::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(SimpleStaticChild::getTime) + .setter(SimpleStaticChild::setTime) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + + // Static Table Schemas for Nested Records + public static TableSchema buildStaticSchemaForNestedRecordWithList() { + return StaticTableSchema.builder(NestedStaticRecordWithList.class) + .newItemSupplier(NestedStaticRecordWithList::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(NestedStaticRecordWithList::getId) + .setter(NestedStaticRecordWithList::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedStaticRecordWithList::getAttr) + .setter(NestedStaticRecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedStaticRecordWithList::getTime) + .setter(NestedStaticRecordWithList::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedStaticLevel2RecordWithList.class, + buildStaticSchemaForNestedLevel2RecordWithList()), + a -> a.name("level2") + .getter(NestedStaticRecordWithList::getLevel2) + .setter(NestedStaticRecordWithList::setLevel2)) + .addAttribute(EnhancedType.listOf(EnhancedType.documentOf( + NestedStaticLevel2RecordWithList.class, + buildStaticSchemaForNestedLevel2RecordWithList())), + a -> a.name("level2List") + .getter(NestedStaticRecordWithList::getLevel2List) + .setter(NestedStaticRecordWithList::setLevel2List)) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedLevel2RecordWithList() { + return StaticTableSchema.builder(NestedStaticLevel2RecordWithList.class) + .newItemSupplier(NestedStaticLevel2RecordWithList::new) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedStaticLevel2RecordWithList::getAttr) + .setter(NestedStaticLevel2RecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedStaticLevel2RecordWithList::getTime) + .setter(NestedStaticLevel2RecordWithList::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedStaticLevel3RecordWithList.class, + buildStaticSchemaForNestedLevel3RecordWithList()), + a -> a.name("level3") + .getter(NestedStaticLevel2RecordWithList::getLevel3) + .setter(NestedStaticLevel2RecordWithList::setLevel3)) + .addAttribute(EnhancedType.listOf(EnhancedType.documentOf( + NestedStaticLevel3RecordWithList.class, + buildStaticSchemaForNestedLevel3RecordWithList())), + a -> a.name("level3List") + .getter(NestedStaticLevel2RecordWithList::getLevel3List) + .setter(NestedStaticLevel2RecordWithList::setLevel3List)) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedLevel3RecordWithList() { + return StaticTableSchema.builder(NestedStaticLevel3RecordWithList.class) + .newItemSupplier(NestedStaticLevel3RecordWithList::new) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedStaticLevel3RecordWithList::getAttr) + .setter(NestedStaticLevel3RecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedStaticLevel3RecordWithList::getTime) + .setter(NestedStaticLevel3RecordWithList::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedStaticLevel4Record.class, + buildStaticSchemaForNestedLevel4Record()), + a -> a.name("level4") + .getter(NestedStaticLevel3RecordWithList::getLevel4) + .setter(NestedStaticLevel3RecordWithList::setLevel4)) + .addAttribute(EnhancedType.listOf( + EnhancedType.documentOf( + NestedStaticLevel4Record.class, + buildStaticSchemaForNestedLevel4Record())), + a -> a.name("level4List") + .getter(NestedStaticLevel3RecordWithList::getLevel4List) + .setter(NestedStaticLevel3RecordWithList::setLevel4List)) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedRecordWithSet() { + return StaticTableSchema.builder(NestedStaticRecordWithSet.class) + .newItemSupplier(NestedStaticRecordWithSet::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(NestedStaticRecordWithSet::getId) + .setter(NestedStaticRecordWithSet::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedStaticRecordWithSet::getAttr) + .setter(NestedStaticRecordWithSet::setAttr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedStaticRecordWithSet::getTime) + .setter(NestedStaticRecordWithSet::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedStaticLevel2RecordWithSet.class, + buildStaticSchemaForNestedLevel2RecordWithSet()), + a -> a.name("level2") + .getter(NestedStaticRecordWithSet::getLevel2) + .setter(NestedStaticRecordWithSet::setLevel2)) + .addAttribute(EnhancedType.setOf(EnhancedType.documentOf( + NestedStaticLevel2RecordWithSet.class, + buildStaticSchemaForNestedLevel2RecordWithSet())), + a -> a.name("level2Set") + .getter(NestedStaticRecordWithSet::getLevel2List) + .setter(NestedStaticRecordWithSet::setLevel2List)) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedLevel2RecordWithSet() { + return StaticTableSchema.builder(NestedStaticLevel2RecordWithSet.class) + .newItemSupplier(NestedStaticLevel2RecordWithSet::new) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedStaticLevel2RecordWithSet::getAttr) + .setter(NestedStaticLevel2RecordWithSet::setAttr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedStaticLevel2RecordWithSet::getTime) + .setter(NestedStaticLevel2RecordWithSet::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedStaticLevel3RecordWithSet.class, + buildStaticSchemaForNestedLevel3RecordWithSet()), + a -> a.name("level3") + .getter(NestedStaticLevel2RecordWithSet::getLevel3) + .setter(NestedStaticLevel2RecordWithSet::setLevel3)) + .addAttribute(EnhancedType.setOf(EnhancedType.documentOf( + NestedStaticLevel3RecordWithSet.class, + buildStaticSchemaForNestedLevel3RecordWithSet())), + a -> a.name("level3Set") + .getter(NestedStaticLevel2RecordWithSet::getLevel3Set) + .setter(NestedStaticLevel2RecordWithSet::setLevel3Set)) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedLevel3RecordWithSet() { + return StaticTableSchema.builder(NestedStaticLevel3RecordWithSet.class) + .newItemSupplier(NestedStaticLevel3RecordWithSet::new) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedStaticLevel3RecordWithSet::getAttr) + .setter(NestedStaticLevel3RecordWithSet::setAttr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedStaticLevel3RecordWithSet::getTime) + .setter(NestedStaticLevel3RecordWithSet::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedStaticLevel4Record.class, + buildStaticSchemaForNestedLevel4Record()), + a -> a.name("level4") + .getter(NestedStaticLevel3RecordWithSet::getLevel4) + .setter(NestedStaticLevel3RecordWithSet::setLevel4)) + .addAttribute(EnhancedType.setOf( + EnhancedType.documentOf( + NestedStaticLevel4Record.class, + buildStaticSchemaForNestedLevel4Record())), + a -> a.name("level4List") + .getter(NestedStaticLevel3RecordWithSet::getLevel4Set) + .setter(NestedStaticLevel3RecordWithSet::setLevel4Set)) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedRecordWithMap() { + return StaticTableSchema.builder(NestedStaticRecordWithMap.class) + .newItemSupplier(NestedStaticRecordWithMap::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(NestedStaticRecordWithMap::getId) + .setter(NestedStaticRecordWithMap::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedStaticRecordWithMap::getAttr) + .setter(NestedStaticRecordWithMap::setAttr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedStaticRecordWithMap::getTime) + .setter(NestedStaticRecordWithMap::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedStaticLevel2RecordWithMap.class, + buildStaticSchemaForNestedLevel2RecordWithMap()), + a -> a.name("level2") + .getter(NestedStaticRecordWithMap::getLevel2) + .setter(NestedStaticRecordWithMap::setLevel2)) + .addAttribute(EnhancedType.mapOf( + String.class, NestedStaticLevel2RecordWithMap.class), + a -> a.name("level2Map") + .getter(NestedStaticRecordWithMap::getLevel2Map) + .setter(NestedStaticRecordWithMap::setLevel2Map)) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedLevel2RecordWithMap() { + return StaticTableSchema.builder(NestedStaticLevel2RecordWithMap.class) + .newItemSupplier(NestedStaticLevel2RecordWithMap::new) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedStaticLevel2RecordWithMap::getAttr) + .setter(NestedStaticLevel2RecordWithMap::setAttr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedStaticLevel2RecordWithMap::getTime) + .setter(NestedStaticLevel2RecordWithMap::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedStaticLevel3RecordWithMap.class, + buildStaticSchemaForNestedLevel3RecordWithMap()), + a -> a.name("level3") + .getter(NestedStaticLevel2RecordWithMap::getLevel3) + .setter(NestedStaticLevel2RecordWithMap::setLevel3)) + .addAttribute(EnhancedType.mapOf( + String.class, NestedStaticLevel3RecordWithMap.class), + a -> a.name("level3Map") + .getter(NestedStaticLevel2RecordWithMap::getLevel3Map) + .setter(NestedStaticLevel2RecordWithMap::setLevel3Map)) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedLevel3RecordWithMap() { + return StaticTableSchema.builder(NestedStaticLevel3RecordWithMap.class) + .newItemSupplier(NestedStaticLevel3RecordWithMap::new) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedStaticLevel3RecordWithMap::getAttr) + .setter(NestedStaticLevel3RecordWithMap::setAttr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedStaticLevel3RecordWithMap::getTime) + .setter(NestedStaticLevel3RecordWithMap::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedStaticLevel4Record.class, + buildStaticSchemaForNestedLevel4Record()), + a -> a.name("level4") + .getter(NestedStaticLevel3RecordWithMap::getLevel4) + .setter(NestedStaticLevel3RecordWithMap::setLevel4)) + .addAttribute(EnhancedType.mapOf( + String.class, NestedStaticLevel4Record.class), + a -> a.name("level4Map") + .getter(NestedStaticLevel3RecordWithMap::getLevel4Map) + .setter(NestedStaticLevel3RecordWithMap::setLevel4Map)) + .build(); + } + + // schema of the nested record used by list/set/map as the deepest nested level + public static TableSchema buildStaticSchemaForNestedLevel4Record() { + return StaticTableSchema.builder(NestedStaticLevel4Record.class) + .newItemSupplier(NestedStaticLevel4Record::new) + .addAttribute(String.class, + x -> x.name("attr") + .getter(NestedStaticLevel4Record::getAttr) + .setter(NestedStaticLevel4Record::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + x -> x.name("time") + .getter(NestedStaticLevel4Record::getTime) + .setter(NestedStaticLevel4Record::setTime) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + + // Static Immutable Table Schemas for Simple Records + public static TableSchema buildStaticImmutableSchemaForSimpleChildRecord() { + return StaticImmutableTableSchema.builder(SimpleImmutableChild.class, SimpleImmutableChild.Builder.class) + .newItemBuilder(SimpleImmutableChild::builder, SimpleImmutableChild.Builder::build) + .addAttribute(String.class, + a -> a.name("id") + .getter(SimpleImmutableChild::getId) + .setter(SimpleImmutableChild.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(SimpleImmutableChild::getAttr) + .setter(SimpleImmutableChild.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(SimpleImmutableChild::getTime) + .setter(SimpleImmutableChild.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForSimpleRecordWithList() { + return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithList.class, + SimpleImmutableRecordWithList.Builder.class) + .newItemBuilder(SimpleImmutableRecordWithList::builder, + SimpleImmutableRecordWithList.Builder::build) + .addAttribute(String.class, + a -> a.name("id") + .getter(SimpleImmutableRecordWithList::getId) + .setter(SimpleImmutableRecordWithList.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(SimpleImmutableRecordWithList::getAttr) + .setter(SimpleImmutableRecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(SimpleImmutableRecordWithList::getTime) + .setter(SimpleImmutableRecordWithList.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.listOf( + EnhancedType.documentOf( + SimpleImmutableChild.class, + buildStaticImmutableSchemaForSimpleChildRecord())), + a -> a.name("childList") + .getter(SimpleImmutableRecordWithList::getChildList) + .setter(SimpleImmutableRecordWithList.Builder::childList)) + .build(); + } + + public static StaticImmutableTableSchema buildStaticImmutableSchemaForSimpleRecordWithSet() { + return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithSet.class, SimpleImmutableRecordWithSet.Builder.class) + .newItemBuilder(SimpleImmutableRecordWithSet::builder, + SimpleImmutableRecordWithSet.Builder::build) + .addAttribute(String.class, + a -> a.name("id") + .getter(SimpleImmutableRecordWithSet::getId) + .setter(SimpleImmutableRecordWithSet.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(SimpleImmutableRecordWithSet::getAttr) + .setter(SimpleImmutableRecordWithSet.Builder::attr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(SimpleImmutableRecordWithSet::getTime) + .setter(SimpleImmutableRecordWithSet.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.setOf( + EnhancedType.documentOf( + SimpleImmutableChild.class, + buildStaticImmutableSchemaForSimpleChildRecord())), + a -> a.name("childSet") + .getter(SimpleImmutableRecordWithSet::getChildSet) + .setter(SimpleImmutableRecordWithSet.Builder::childSet)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForSimpleRecordWithMap() { + return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithMap.class, SimpleImmutableRecordWithMap.Builder.class) + .newItemBuilder(SimpleImmutableRecordWithMap::builder, + SimpleImmutableRecordWithMap.Builder::build) + .addAttribute(String.class, + a -> a.name("id") + .getter(SimpleImmutableRecordWithMap::getId) + .setter(SimpleImmutableRecordWithMap.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(SimpleImmutableRecordWithMap::getAttr) + .setter(SimpleImmutableRecordWithMap.Builder::attr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(SimpleImmutableRecordWithMap::getTime) + .setter(SimpleImmutableRecordWithMap.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.mapOf( + String.class, SimpleImmutableChild.class), + a -> a.name("childMap") + .getter(SimpleImmutableRecordWithMap::getChildMap) + .setter(SimpleImmutableRecordWithMap.Builder::childMap)) + .build(); + } + + + // Static Immutable Table Schemas for Nested Records + public static TableSchema buildStaticImmutableSchemaForNestedRecordWithList() { + return StaticImmutableTableSchema.builder(NestedImmutableRecordWithList.class, + NestedImmutableRecordWithList.Builder.class) + .newItemBuilder(NestedImmutableRecordWithList::builder, + NestedImmutableRecordWithList.Builder::build) + .addAttribute(String.class, + a -> a.name("id") + .getter(NestedImmutableRecordWithList::getId) + .setter(NestedImmutableRecordWithList.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableRecordWithList::getAttr) + .setter(NestedImmutableRecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableRecordWithList::getTime) + .setter(NestedImmutableRecordWithList.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedImmutableLevel2RecordWithList.class, + buildStaticImmutableSchemaForNestedLevel2RecordWithList()), + a -> a.name("level2") + .getter(NestedImmutableRecordWithList::getLevel2) + .setter(NestedImmutableRecordWithList.Builder::level2)) + .addAttribute(EnhancedType.listOf( + EnhancedType.documentOf( + NestedImmutableLevel2RecordWithList.class, + buildStaticImmutableSchemaForNestedLevel2RecordWithList())), + a -> a.name("level2List") + .getter(NestedImmutableRecordWithList::getLevel2List) + .setter(NestedImmutableRecordWithList.Builder::level2List)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedLevel2RecordWithList() { + return StaticImmutableTableSchema.builder(NestedImmutableLevel2RecordWithList.class, + NestedImmutableLevel2RecordWithList.Builder.class) + .newItemBuilder(NestedImmutableLevel2RecordWithList::builder, + NestedImmutableLevel2RecordWithList.Builder::build) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableLevel2RecordWithList::getAttr) + .setter(NestedImmutableLevel2RecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableLevel2RecordWithList::getTime) + .setter(NestedImmutableLevel2RecordWithList.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedImmutableLevel3RecordWithList.class, + buildStaticImmutableSchemaForNestedLevel3RecordWithList()), + a -> a.name("level3") + .getter(NestedImmutableLevel2RecordWithList::getLevel3) + .setter(NestedImmutableLevel2RecordWithList.Builder::level3)) + .addAttribute(EnhancedType.listOf( + EnhancedType.documentOf( + NestedImmutableLevel3RecordWithList.class, + buildStaticImmutableSchemaForNestedLevel3RecordWithList())), + a -> a.name("level3List") + .getter(NestedImmutableLevel2RecordWithList::getLevel3List) + .setter(NestedImmutableLevel2RecordWithList.Builder::level3List)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedLevel3RecordWithList() { + return StaticImmutableTableSchema.builder(NestedImmutableLevel3RecordWithList.class, + NestedImmutableLevel3RecordWithList.Builder.class) + .newItemBuilder(NestedImmutableLevel3RecordWithList::builder, + NestedImmutableLevel3RecordWithList.Builder::build) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableLevel3RecordWithList::getAttr) + .setter(NestedImmutableLevel3RecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableLevel3RecordWithList::getTime) + .setter(NestedImmutableLevel3RecordWithList.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedImmutableLevel4Record.class, + buildStaticImmutableSchemaForNestedLevel4Record()), + a -> a.name("level4") + .getter(NestedImmutableLevel3RecordWithList::getLevel4) + .setter(NestedImmutableLevel3RecordWithList.Builder::level4)) + .addAttribute(EnhancedType.listOf( + EnhancedType.documentOf( + NestedImmutableLevel4Record.class, + buildStaticImmutableSchemaForNestedLevel4Record())), + a -> a.name("level4List") + .getter(NestedImmutableLevel3RecordWithList::getLevel4List) + .setter(NestedImmutableLevel3RecordWithList.Builder::level4List)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedRecordWithSet() { + return StaticImmutableTableSchema.builder(NestedImmutableRecordWithSet.class, + NestedImmutableRecordWithSet.Builder.class) + .newItemBuilder(NestedImmutableRecordWithSet::builder, + NestedImmutableRecordWithSet.Builder::build) + .addAttribute(String.class, + a -> a.name("id") + .getter(NestedImmutableRecordWithSet::getId) + .setter(NestedImmutableRecordWithSet.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableRecordWithSet::getAttr) + .setter(NestedImmutableRecordWithSet.Builder::attr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableRecordWithSet::getTime) + .setter(NestedImmutableRecordWithSet.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedImmutableLevel2RecordWithSet.class, + buildStaticImmutableSchemaForNestedLevel2RecordWithSet()), + a -> a.name("level2") + .getter(NestedImmutableRecordWithSet::getLevel2) + .setter(NestedImmutableRecordWithSet.Builder::level2)) + .addAttribute(EnhancedType.setOf( + EnhancedType.documentOf( + NestedImmutableLevel2RecordWithSet.class, + buildStaticImmutableSchemaForNestedLevel2RecordWithSet())), + a -> a.name("level2Set") + .getter(NestedImmutableRecordWithSet::getLevel2Set) + .setter(NestedImmutableRecordWithSet.Builder::level2Set)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedLevel2RecordWithSet() { + return StaticImmutableTableSchema.builder(NestedImmutableLevel2RecordWithSet.class, + NestedImmutableLevel2RecordWithSet.Builder.class) + .newItemBuilder(NestedImmutableLevel2RecordWithSet::builder, + NestedImmutableLevel2RecordWithSet.Builder::build) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableLevel2RecordWithSet::getAttr) + .setter(NestedImmutableLevel2RecordWithSet.Builder::attr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableLevel2RecordWithSet::getTime) + .setter(NestedImmutableLevel2RecordWithSet.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedImmutableLevel3RecordWithSet.class, + buildStaticImmutableSchemaForNestedLevel3RecordWithSet()), + a -> a.name("level3") + .getter(NestedImmutableLevel2RecordWithSet::getLevel3) + .setter(NestedImmutableLevel2RecordWithSet.Builder::level3)) + .addAttribute(EnhancedType.setOf( + EnhancedType.documentOf( + NestedImmutableLevel3RecordWithSet.class, + buildStaticImmutableSchemaForNestedLevel3RecordWithSet())), + a -> a.name("level3Set") + .getter(NestedImmutableLevel2RecordWithSet::getLevel3Set) + .setter(NestedImmutableLevel2RecordWithSet.Builder::level3Set)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedLevel3RecordWithSet() { + return StaticImmutableTableSchema.builder(NestedImmutableLevel3RecordWithSet.class, + NestedImmutableLevel3RecordWithSet.Builder.class) + .newItemBuilder(NestedImmutableLevel3RecordWithSet::builder, + NestedImmutableLevel3RecordWithSet.Builder::build) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableLevel3RecordWithSet::getAttr) + .setter(NestedImmutableLevel3RecordWithSet.Builder::attr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableLevel3RecordWithSet::getTime) + .setter(NestedImmutableLevel3RecordWithSet.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedImmutableLevel4Record.class, + buildStaticImmutableSchemaForNestedLevel4Record()), + a -> a.name("level4") + .getter(NestedImmutableLevel3RecordWithSet::getLevel4) + .setter(NestedImmutableLevel3RecordWithSet.Builder::level4)) + .addAttribute(EnhancedType.setOf(EnhancedType.documentOf(NestedImmutableLevel4Record.class, + buildStaticImmutableSchemaForNestedLevel4Record())), + a -> a.name("level4Set") + .getter(NestedImmutableLevel3RecordWithSet::getLevel4Set) + .setter(NestedImmutableLevel3RecordWithSet.Builder::level4Set)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedRecordWithMap() { + return StaticImmutableTableSchema.builder(NestedImmutableRecordWithMap.class, + NestedImmutableRecordWithMap.Builder.class) + .newItemBuilder(NestedImmutableRecordWithMap::builder, + NestedImmutableRecordWithMap.Builder::build) + .addAttribute(String.class, + a -> a.name("id") + .getter(NestedImmutableRecordWithMap::getId) + .setter(NestedImmutableRecordWithMap.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableRecordWithMap::getAttr) + .setter(NestedImmutableRecordWithMap.Builder::attr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableRecordWithMap::getTime) + .setter(NestedImmutableRecordWithMap.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedImmutableLevel2RecordWithMap.class, + buildStaticImmutableSchemaForNestedLevel2RecordWithMap()), + a -> a.name("level2") + .getter(NestedImmutableRecordWithMap::getLevel2) + .setter(NestedImmutableRecordWithMap.Builder::level2)) + .addAttribute(EnhancedType.mapOf( + String.class, NestedImmutableLevel2RecordWithMap.class), + a -> a.name("childMap") + .getter(NestedImmutableRecordWithMap::getLevel2Map) + .setter(NestedImmutableRecordWithMap.Builder::level2Map)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedLevel2RecordWithMap() { + return StaticImmutableTableSchema.builder(NestedImmutableLevel2RecordWithMap.class, + NestedImmutableLevel2RecordWithMap.Builder.class) + .newItemBuilder(NestedImmutableLevel2RecordWithMap::builder, + NestedImmutableLevel2RecordWithMap.Builder::build) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableLevel2RecordWithMap::getAttr) + .setter(NestedImmutableLevel2RecordWithMap.Builder::attr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableLevel2RecordWithMap::getTime) + .setter(NestedImmutableLevel2RecordWithMap.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedImmutableLevel3RecordWithMap.class, + buildStaticImmutableSchemaForNestedLevel3RecordWithMap()), + a -> a.name("level3") + .getter(NestedImmutableLevel2RecordWithMap::getLevel3) + .setter(NestedImmutableLevel2RecordWithMap.Builder::level3)) + .addAttribute(EnhancedType.mapOf( + String.class, NestedImmutableLevel3RecordWithMap.class), + a -> a.name("childMap") + .getter(NestedImmutableLevel2RecordWithMap::getLevel3Map) + .setter(NestedImmutableLevel2RecordWithMap.Builder::level3Map)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedLevel3RecordWithMap() { + return StaticImmutableTableSchema.builder(NestedImmutableLevel3RecordWithMap.class, + NestedImmutableLevel3RecordWithMap.Builder.class) + .newItemBuilder(NestedImmutableLevel3RecordWithMap::builder, + NestedImmutableLevel3RecordWithMap.Builder::build) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableLevel3RecordWithMap::getAttr) + .setter(NestedImmutableLevel3RecordWithMap.Builder::attr)) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableLevel3RecordWithMap::getTime) + .setter(NestedImmutableLevel3RecordWithMap.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf( + NestedImmutableLevel4Record.class, + buildStaticImmutableSchemaForNestedLevel4Record()), + a -> a.name("level4") + .getter(NestedImmutableLevel3RecordWithMap::getLevel4) + .setter(NestedImmutableLevel3RecordWithMap.Builder::level4)) + .addAttribute(EnhancedType.mapOf( + String.class, NestedImmutableLevel4Record.class), + a -> a.name("level4Map") + .getter(NestedImmutableLevel3RecordWithMap::getLevel4Map) + .setter(NestedImmutableLevel3RecordWithMap.Builder::level4Map)) + .build(); + } + + // schema of the record used by list/set/map as the deepest nested level + public static TableSchema buildStaticImmutableSchemaForNestedLevel4Record() { + return StaticImmutableTableSchema.builder(NestedImmutableLevel4Record.class, NestedImmutableLevel4Record.Builder.class) + .newItemBuilder(NestedImmutableLevel4Record::builder, + NestedImmutableLevel4Record.Builder::build) + .addAttribute(String.class, + a -> a.name("attr") + .getter(NestedImmutableLevel4Record::getAttr) + .setter(NestedImmutableLevel4Record.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, + a -> a.name("time") + .getter(NestedImmutableLevel4Record::getTime) + .setter(NestedImmutableLevel4Record.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + + // Object builder methods + public static SimpleBeanRecordWithList buildSimpleBeanRecordWithList() { + return new SimpleBeanRecordWithList() + .setId(ID_1).setAttr(ATTR_LEVEL1) + .setChildList(new ArrayList<>(Arrays.asList( + new SimpleBeanChild().setId(ID_1).setAttr(ATTR_CHILD1), + new SimpleBeanChild().setId(ID_2).setAttr(ATTR_CHILD2)))); + } + + public static SimpleStaticRecordWithList buildSimpleStaticRecordWithList() { + return new SimpleStaticRecordWithList() + .setId(ID_1).setAttr(ATTR_LEVEL1) + .setChildList(new ArrayList<>(Arrays.asList( + new SimpleStaticChild().setId(ID_1).setAttr(ATTR_CHILD1), + new SimpleStaticChild().setId(ID_2).setAttr(ATTR_CHILD2)))); + } + + public static SimpleImmutableRecordWithList buildSimpleImmutableRecordWithList() { + return SimpleImmutableRecordWithList.builder() + .id(ID_1).attr(ATTR_LEVEL1) + .childList(new ArrayList<>(Arrays.asList( + SimpleImmutableChild.builder().id(ID_1).attr(ATTR_CHILD1).build(), + SimpleImmutableChild.builder().id(ID_2).attr(ATTR_CHILD2).build()))) + .build(); + } + + public static NestedBeanRecordWithList buildNestedBeanRecordWithList() { + NestedBeanLevel4Record level4 = + new NestedBeanLevel4Record() + .setId(ID_1) + .setAttr(ATTR_LEVEL4); + + NestedBeanLevel3RecordWithList level3 = + new NestedBeanLevel3RecordWithList() + .setAttr(ATTR_LEVEL3) + .setLevel4(level4) + .setLevel4List(new ArrayList<>(singletonList(level4))); + + NestedBeanLevel2RecordWithList level2 = + new NestedBeanLevel2RecordWithList() + .setAttr(ATTR_LEVEL2).setLevel3(level3) + .setLevel3List(new ArrayList<>(singletonList(level3))); + + return new NestedBeanRecordWithList() + .setId(ID_1).setAttr(ATTR_LEVEL1) + .setLevel2(level2) + .setLevel2List(new ArrayList<>(singletonList(level2))); + } + + public static NestedStaticRecordWithList buildNestedStaticRecordWithList() { + NestedStaticLevel4Record level4 = + new NestedStaticLevel4Record() + .setAttr(ATTR_LEVEL4); + + NestedStaticLevel3RecordWithList level3 = + new NestedStaticLevel3RecordWithList() + .setAttr(ATTR_LEVEL3) + .setLevel4(level4) + .setLevel4List(new ArrayList<>(singletonList(level4))); + + NestedStaticLevel2RecordWithList level2 = + new NestedStaticLevel2RecordWithList() + .setAttr(ATTR_LEVEL2) + .setLevel3(level3) + .setLevel3List(new ArrayList<>(singletonList(level3))); + + return new NestedStaticRecordWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setLevel2(level2) + .setLevel2List(new ArrayList<>(singletonList(level2))); + } + + public static NestedImmutableRecordWithList buildNestedImmutableRecordWithList() { + NestedImmutableLevel4Record level4 = + NestedImmutableLevel4Record.builder() + .id(ID_ATTR) + .attr(ATTR_LEVEL4) + .build(); + + NestedImmutableLevel3RecordWithList level3 = + NestedImmutableLevel3RecordWithList.builder() + .attr(ATTR_LEVEL3) + .level4(level4) + .level4List(new ArrayList<>(singletonList(level4))) + .build(); + + NestedImmutableLevel2RecordWithList level2 = + NestedImmutableLevel2RecordWithList.builder() + .attr(ATTR_LEVEL2) + .level3(level3) + .level3List(new ArrayList<>(singletonList(level3))) + .build(); + + return NestedImmutableRecordWithList.builder() + .id(ID_1) + .attr(ATTR_LEVEL1).level2(level2) + .level2List(new ArrayList<>(singletonList(level2))) + .build(); + } + + public static SimpleBeanRecordWithMap buildSimpleBeanRecordWithMap() { + return new SimpleBeanRecordWithMap() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setChildMap(ImmutableMap.of( + CHILD1_KEY, new SimpleBeanChild().setId(ID_1).setAttr(ATTR_CHILD1), + CHILD2_KEY, new SimpleBeanChild().setId(ID_2).setAttr(ATTR_CHILD2) + )); + } + + public static NestedBeanRecordWithMap buildNestedBeanRecordWithMap() { + NestedBeanLevel4Record level4 = + new NestedBeanLevel4Record() + .setId(ID_1) + .setAttr(ATTR_LEVEL4); + + NestedBeanLevel3RecordWithMap level3 = + new NestedBeanLevel3RecordWithMap() + .setAttr(ATTR_LEVEL3) + .setLevel4(level4) + .setLevel4Map(ImmutableMap.of(LEVEL4_KEY, level4)); + + NestedBeanLevel2RecordWithMap level2 = + new NestedBeanLevel2RecordWithMap() + .setAttr(ATTR_LEVEL2) + .setLevel3(level3) + .setLevel3Map(ImmutableMap.of(LEVEL3_KEY, level3)); + + return new NestedBeanRecordWithMap() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setLevel2(level2) + .setLevel2Map(ImmutableMap.of(LEVEL2_KEY, level2)); + } + + public static SimpleImmutableRecordWithMap buildSimpleImmutableRecordWithMap() { + return SimpleImmutableRecordWithMap.builder() + .id(ID_1).attr(ATTR_LEVEL1) + .childMap(ImmutableMap.of( + CHILD1_KEY, SimpleImmutableChild.builder().id(ID_1).attr(ATTR_CHILD1).build(), + CHILD2_KEY, SimpleImmutableChild.builder().id(ID_2).attr(ATTR_CHILD2).build())) + .build(); + } + + public static NestedImmutableRecordWithMap buildNestedImmutableRecordWithMap() { + NestedImmutableLevel4Record level4 = + NestedImmutableLevel4Record.builder() + .id(ID_1) + .attr(ATTR_LEVEL4) + .build(); + + NestedImmutableLevel3RecordWithMap level3 = + NestedImmutableLevel3RecordWithMap.builder() + .attr(ATTR_LEVEL3) + .level4(level4) + .level4Map(new HashMap<>(singletonMap(LEVEL4_KEY, level4))) + .build(); + + NestedImmutableLevel2RecordWithMap level2 = + NestedImmutableLevel2RecordWithMap.builder() + .attr(ATTR_LEVEL2) + .level3(level3) + .level3Map(new HashMap<>(singletonMap(LEVEL3_KEY, level3))) + .build(); + + return NestedImmutableRecordWithMap.builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .level2(level2) + .level2Map(new HashMap<>(singletonMap(LEVEL2_KEY, level2))) + .build(); + } +} \ No newline at end of file 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..ad396ed28d00 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 @@ -15,7 +15,10 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; +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.converters.EpochMillisFormatTestConverter; import software.amazon.awssdk.enhanced.dynamodb.converters.TimeFormatUpdateTestConverter; import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; @@ -26,8 +29,6 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS; - @DynamoDbBean public class RecordWithUpdateBehaviors { private String id; @@ -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; + } } From 56d0d49962f4d7f4ad79c1628d40677b2320c3be Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Tue, 3 Feb 2026 09:06:35 +0200 Subject: [PATCH 02/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- ...AutoGeneratedTimestampRecordExtension.java | 64 +++++++++---- ...namoDbAutoGeneratedTimestampAttribute.java | 4 + .../operations/UpdateItemOperation.java | 12 ++- .../annotations/DynamoDbUpdateBehavior.java | 4 + .../AutoGeneratedTimestampRecordTest.java | 65 +++++++++++++- .../functionaltests/UpdateBehaviorTest.java | 56 +++++++++++- ...WithInvalidAttributeNameOnNestedLevel.java | 89 +++++++++++++++++++ .../BeanWithInvalidAttributeNameOnRoot.java | 50 +++++++++++ ...WithInvalidAttributeNameOnNestedLevel.java | 79 ++++++++++++++++ .../RecordWithInvalidAttributeNameOnRoot.java | 42 +++++++++ 10 files changed, 444 insertions(+), 21 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnNestedLevel.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnRoot.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnNestedLevel.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnRoot.java 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 27bf8065a015..09e0f403bcea 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 @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -81,6 +82,8 @@ @SdkPublicApi @ThreadSafe public final class AutoGeneratedTimestampRecordExtension implements DynamoDbEnhancedClientExtension { + + private static final String NESTED_OBJECT_UPDATE = "_NESTED_ATTR_UPDATE_"; private static final String CUSTOM_METADATA_KEY = "AutoGeneratedTimestampExtension:AutoGeneratedTimestampAttribute"; private static final AutoGeneratedTimestampAttribute AUTO_GENERATED_TIMESTAMP_ATTRIBUTE = new AutoGeneratedTimestampAttribute(); @@ -151,19 +154,28 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex 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()); + } else if (value.hasL() && !value.l().isEmpty()) { + // Check first non-null element to determine if this is a list of maps + AttributeValue firstElement = value.l().stream() + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + + if (firstElement != null && firstElement.hasM()) { + TableSchema elementListSchema = getTableSchemaForListElement(context.tableSchema(), key); + + List updatedList = + value.l() + .stream() + .map(listItem -> (listItem != null && listItem.hasM()) ? + AttributeValue.builder() + .m(processNestedObject(listItem.m(), + elementListSchema, + currentInstant)) + .build() : listItem) + .collect(Collectors.toList()); + updatedItems.put(key, AttributeValue.builder().l(updatedList).build()); + } } }); @@ -208,11 +220,16 @@ private Map processNestedObject(Map { 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()); + if (childSchemaOptional.isPresent()) { + updatedNestedMap.put( + nestedKey, + AttributeValue.builder() + .m(processNestedObject(nestedValue.m(), childSchemaOptional.get(), currentInstant)) + .build()); + } else { + // No schema available for this nested object - skip timestamp processing but preserve the object + updatedNestedMap.put(nestedKey, nestedValue); + } } else if (nestedValue.hasL() && !nestedValue.l().isEmpty() && nestedValue.l().get(0).hasM()) { TableSchema listElementSchema = getTableSchemaForListElement(nestedSchema, nestedKey); @@ -280,6 +297,7 @@ public void validateType(String attributeName, EnhancedType type, Validate.notNull(type, "type is null"); Validate.notNull(type.rawClass(), "rawClass is null"); Validate.notNull(attributeValueType, "attributeValueType is null"); + validateAttributeName(attributeName); if (!type.rawClass().equals(Instant.class)) { throw new IllegalArgumentException(String.format( @@ -294,5 +312,15 @@ public Consumer modifyMetadata(String attributeName return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, Collections.singleton(attributeName)) .markAttributeAsKey(attributeName, attributeValueType); } + + private static void validateAttributeName(String attributeName) { + if (attributeName.contains(NESTED_OBJECT_UPDATE)) { + throw new IllegalArgumentException( + String.format( + "Attribute name '%s' contains reserved marker '%s' and is not allowed.", + attributeName, + NESTED_OBJECT_UPDATE)); + } + } } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedTimestampAttribute.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedTimestampAttribute.java index 065edfd2a297..3c8e48085001 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedTimestampAttribute.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbAutoGeneratedTimestampAttribute.java @@ -27,6 +27,10 @@ * Denotes this attribute as recording the auto generated last updated timestamp for the record. * Every time a record with this attribute is written to the database it will update the attribute with current timestamp when * its updated. + *

+ * Note: This annotation must not be applied to fields whose names contain the reserved marker "_NESTED_ATTR_UPDATE_". + * This marker is used internally by the Enhanced Client to represent flattened paths for nested attribute updates. + * If a field name contains this marker, an IllegalArgumentException will be thrown during schema registration. */ @SdkPublicApi @Target({ElementType.METHOD}) 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 cd98db7417bc..3b089446736a 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 @@ -171,6 +171,7 @@ public Map transformItemToMapForUpdateExpression(Map nestedAttributes = new HashMap<>(); itemToMap.forEach((key, value) -> { + validateAttributeName(key); if (value.hasM() && isNotEmptyMap(value.m())) { nestedAttributes.put(key, value); } @@ -192,8 +193,9 @@ private Map nestedItemToMap(Map String key, AttributeValue attributeValue) { attributeValue.m().forEach((mapKey, mapValue) -> { - String nestedAttributeKey = key + NESTED_OBJECT_UPDATE + mapKey; + validateAttributeName(mapKey); if (attributeValueNonNullOrShouldWriteNull(mapValue)) { + String nestedAttributeKey = key + NESTED_OBJECT_UPDATE + mapKey; if (mapValue.hasM()) { nestedItemToMap(itemToMap, nestedAttributeKey, mapValue); } else { @@ -355,4 +357,12 @@ private static Map coalesceExpressionValues(Expression f } return expressionValues; } + + private static void validateAttributeName(String attributeName) { + if (attributeName.contains(NESTED_OBJECT_UPDATE)) { + throw new IllegalArgumentException( + String.format("Attribute name '%s' contains reserved marker '%s' and is not allowed.", + attributeName, NESTED_OBJECT_UPDATE)); + } + } } 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 d14216b6a529..7adc4d0006a7 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 @@ -31,6 +31,10 @@ * {@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. + *

+ * Note: This annotation must not be applied to fields whose names contain the reserved marker "_NESTED_ATTR_UPDATE_". + * This marker is used internally by the Enhanced Client to represent flattened paths for nested attribute updates. + * If a field name contains this marker, an IllegalArgumentException will be thrown during schema registration. */ @SdkPublicApi @Target({ElementType.METHOD}) 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 e1f9835ec1e3..3047ca927379 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 @@ -26,6 +26,7 @@ import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute; import java.time.Clock; import java.time.Instant; @@ -48,6 +49,8 @@ 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.functionaltests.models.RecordWithInvalidAttributeNameOnRoot; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; @@ -438,7 +441,7 @@ public void updateItemConditionTestFailure() { } @Test - public void incorrectTypeForAutoUpdateTimestampThrowsException() { + public void autogenerateTimestamps_onItemWithNonInstantAttributeName_throwsException() { thrown.expect(IllegalArgumentException.class); thrown.expectMessage("Attribute 'lastUpdatedDate' of Class type class java.lang.String is not a suitable " @@ -457,6 +460,66 @@ public void incorrectTypeForAutoUpdateTimestampThrowsException() { .build(); } + @Test + public void autogenerateTimestamps_onItemWithRootAttributeNameContainingReservedMarker_throwsException() { + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Attribute name 'attr_NESTED_ATTR_UPDATE_' contains reserved marker " + + "'_NESTED_ATTR_UPDATE_' and is not allowed."); + + StaticTableSchema + .builder(RecordWithInvalidAttributeNameOnRoot.class) + .newItemSupplier(RecordWithInvalidAttributeNameOnRoot::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(RecordWithInvalidAttributeNameOnRoot::getId) + .setter(RecordWithInvalidAttributeNameOnRoot::setId) + .tags(primaryPartitionKey())) + + .addAttribute(Instant.class, + a -> a.name("attr_NESTED_ATTR_UPDATE_") + .getter(RecordWithInvalidAttributeNameOnRoot::getAttr_NESTED_ATTR_UPDATE_) + .setter(RecordWithInvalidAttributeNameOnRoot::setAttr_NESTED_ATTR_UPDATE_) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + @Test + public void autogenerateTimestamps_onItemWithNestedAttributeNameContainingReservedMarker_throwsException() { + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Attribute name 'childAttr_NESTED_ATTR_UPDATE_' contains reserved marker " + + "'_NESTED_ATTR_UPDATE_' and is not allowed."); + + StaticTableSchema + .builder(RecordWithInvalidAttributeNameOnNestedLevel.class) + .newItemSupplier(RecordWithInvalidAttributeNameOnNestedLevel::new) + .addAttribute( + String.class, + a -> a.name("id") + .getter(RecordWithInvalidAttributeNameOnNestedLevel::getId) + .setter(RecordWithInvalidAttributeNameOnNestedLevel::setId) + .tags(primaryPartitionKey())) + + .addAttribute( + EnhancedType.documentOf( + RecordWithReservedMarkerNestedChildAttribute.class, + StaticTableSchema + .builder(RecordWithReservedMarkerNestedChildAttribute.class) + .newItemSupplier(RecordWithReservedMarkerNestedChildAttribute::new) + .addAttribute(Instant.class, + a -> a.name("childAttr_NESTED_ATTR_UPDATE_") + .getter(RecordWithReservedMarkerNestedChildAttribute::getAttr_NESTED_ATTR_UPDATE_) + .setter(RecordWithReservedMarkerNestedChildAttribute::setAttr_NESTED_ATTR_UPDATE_) + .tags(autoGeneratedTimestampAttribute())) + .build()), + + a -> a.name("nestedChildAttribute") + .getter(RecordWithInvalidAttributeNameOnNestedLevel::getNestedChildAttribute) + .setter(RecordWithInvalidAttributeNameOnNestedLevel::setNestedChildAttribute)) + .build(); + } + @Test public void putItemFollowedByUpdatesShouldGenerateTimestampsOnNestedFields() { mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one") 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 15342c3f25e5..e40c9bc7045c 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 @@ -3,6 +3,7 @@ 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 static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.BeanWithInvalidAttributeNameOnNestedLevel.BeanChildWithInvalidAttributeNameOnNestedLevel; import com.google.common.collect.ImmutableList; import java.time.Instant; @@ -13,11 +14,15 @@ import java.util.stream.Stream; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; 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.AutoGeneratedTimestampRecordExtension; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.BeanWithInvalidAttributeNameOnNestedLevel; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.BeanWithInvalidAttributeNameOnRoot; 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; @@ -43,6 +48,12 @@ public class UpdateBehaviorTest extends LocalDynamoDbSyncTestBase { private static final TableSchema TABLE_SCHEMA_FLATTEN_RECORD = TableSchema.fromClass(FlattenRecord.class); + private static final TableSchema TABLE_SCHEMA_INVALID_ATTR_NAME_ON_ROOT = + TableSchema.fromClass(BeanWithInvalidAttributeNameOnRoot.class); + + private static final TableSchema TABLE_SCHEMA_INVALID_ATTR_NAME_ON_NESTED_LEVEL = + TableSchema.fromClass(BeanWithInvalidAttributeNameOnNestedLevel.class); + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()).extensions( Stream.concat(ExtensionResolver.defaultExtensions().stream(), @@ -51,10 +62,19 @@ public class UpdateBehaviorTest extends LocalDynamoDbSyncTestBase { private final DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); - + private final DynamoDbTable flattenedMappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_FLATTEN_RECORD); + private final DynamoDbTable beanWithInvalidRootAttrNameMappedTable = + enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_INVALID_ATTR_NAME_ON_ROOT); + + private final DynamoDbTable beanWithInvalidNestedAttrNameMappedTable = + enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_INVALID_ATTR_NAME_ON_NESTED_LEVEL); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Before public void createTable() { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); @@ -743,4 +763,38 @@ public void updateBehaviors_nested() { assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isNull(); assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isNotNull(); } + + @Test + public void updateBehaviour_onItemWithRootAttributeNameContainingReservedMarker_throwsException() { + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Attribute name 'attr_NESTED_ATTR_UPDATE_' contains reserved marker " + + "'_NESTED_ATTR_UPDATE_' and is not allowed."); + + BeanWithInvalidAttributeNameOnRoot record = new BeanWithInvalidAttributeNameOnRoot(); + record.setId("1"); + record.setAttr_NESTED_ATTR_UPDATE_(INSTANT_1); + + beanWithInvalidRootAttrNameMappedTable.updateItem(r -> r.item(record) + .ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + } + + @Test + public void updateBehaviour_onItemWithNestedAttributeNameContainingReservedMarker_throwsException() { + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Attribute name 'attr_NESTED_ATTR_UPDATE_' contains reserved marker " + + "'_NESTED_ATTR_UPDATE_' and is not allowed."); + + BeanWithInvalidAttributeNameOnNestedLevel record = new BeanWithInvalidAttributeNameOnNestedLevel(); + record.setId("1"); + + BeanChildWithInvalidAttributeNameOnNestedLevel childBean = new BeanChildWithInvalidAttributeNameOnNestedLevel(); + childBean.setId("2"); + childBean.setAttr_NESTED_ATTR_UPDATE_(INSTANT_1); + record.setNestedChildAttribute(childBean); + + beanWithInvalidNestedAttrNameMappedTable.updateItem(r -> r.item(record) + .ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnNestedLevel.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnNestedLevel.java new file mode 100644 index 000000000000..827151c5ec68 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnNestedLevel.java @@ -0,0 +1,89 @@ +/* + * 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 static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS; + +import java.time.Instant; +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.DynamoDbUpdateBehavior; + +@DynamoDbBean +public class BeanWithInvalidAttributeNameOnNestedLevel { + + private String id; + private BeanChildWithInvalidAttributeNameOnNestedLevel nestedChildAttribute; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public BeanWithInvalidAttributeNameOnNestedLevel setId(String id) { + this.id = id; + return this; + } + + public BeanChildWithInvalidAttributeNameOnNestedLevel getNestedChildAttribute() { + return nestedChildAttribute; + } + + public BeanWithInvalidAttributeNameOnNestedLevel setNestedChildAttribute( + BeanChildWithInvalidAttributeNameOnNestedLevel nestedChildAttribute) { + this.nestedChildAttribute = nestedChildAttribute; + return this; + } + + + @DynamoDbBean + public static class BeanChildWithInvalidAttributeNameOnNestedLevel { + + private String id; + private BeanChildWithInvalidAttributeNameOnNestedLevel nestedChildAttribute; + private Instant childAttr_NESTED_ATTR_UPDATE_; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public BeanChildWithInvalidAttributeNameOnNestedLevel setId(String id) { + this.id = id; + return this; + } + + public BeanChildWithInvalidAttributeNameOnNestedLevel getNestedChildAttribute() { + return nestedChildAttribute; + } + + public BeanChildWithInvalidAttributeNameOnNestedLevel setNestedChildAttribute( + BeanChildWithInvalidAttributeNameOnNestedLevel nestedChildAttribute) { + this.nestedChildAttribute = nestedChildAttribute; + return this; + } + + @DynamoDbUpdateBehavior(WRITE_IF_NOT_EXISTS) + public Instant getAttr_NESTED_ATTR_UPDATE_() { + return childAttr_NESTED_ATTR_UPDATE_; + } + + public BeanChildWithInvalidAttributeNameOnNestedLevel setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + this.childAttr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; + return this; + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnRoot.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnRoot.java new file mode 100644 index 000000000000..14f4f4c46baf --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnRoot.java @@ -0,0 +1,50 @@ +/* + * 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 static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS; + +import java.time.Instant; +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.DynamoDbUpdateBehavior; + +@DynamoDbBean +public class BeanWithInvalidAttributeNameOnRoot { + + private String id; + private Instant attr_NESTED_ATTR_UPDATE_; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public BeanWithInvalidAttributeNameOnRoot setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(WRITE_IF_NOT_EXISTS) + public Instant getAttr_NESTED_ATTR_UPDATE_() { + return attr_NESTED_ATTR_UPDATE_; + } + + public BeanWithInvalidAttributeNameOnRoot setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + this.attr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; + return this; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnNestedLevel.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnNestedLevel.java new file mode 100644 index 000000000000..8232f802a531 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnNestedLevel.java @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import java.time.Instant; + +public class RecordWithInvalidAttributeNameOnNestedLevel { + + private String id; + private RecordWithReservedMarkerNestedChildAttribute nestedChildAttribute; + + public String getId() { + return id; + } + + public RecordWithInvalidAttributeNameOnNestedLevel setId(String id) { + this.id = id; + return this; + } + + public RecordWithReservedMarkerNestedChildAttribute getNestedChildAttribute() { + return nestedChildAttribute; + } + + public RecordWithInvalidAttributeNameOnNestedLevel setNestedChildAttribute( + RecordWithReservedMarkerNestedChildAttribute nestedChildAttribute) { + this.nestedChildAttribute = nestedChildAttribute; + return this; + } + + + public static class RecordWithReservedMarkerNestedChildAttribute { + + private String id; + private RecordWithReservedMarkerNestedChildAttribute nestedChildAttribute; + private Instant childAttr_NESTED_ATTR_UPDATE_; + + public String getId() { + return id; + } + + public RecordWithReservedMarkerNestedChildAttribute setId(String id) { + this.id = id; + return this; + } + + public RecordWithReservedMarkerNestedChildAttribute getNestedChildAttribute() { + return nestedChildAttribute; + } + + public RecordWithReservedMarkerNestedChildAttribute setNestedChildAttribute( + RecordWithReservedMarkerNestedChildAttribute nestedChildAttribute) { + this.nestedChildAttribute = nestedChildAttribute; + return this; + } + + public Instant getAttr_NESTED_ATTR_UPDATE_() { + return childAttr_NESTED_ATTR_UPDATE_; + } + + public RecordWithReservedMarkerNestedChildAttribute setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + this.childAttr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; + return this; + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnRoot.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnRoot.java new file mode 100644 index 000000000000..faf5d8fa8550 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnRoot.java @@ -0,0 +1,42 @@ +/* + * 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; + +public class RecordWithInvalidAttributeNameOnRoot { + + private String id; + private Instant attr_NESTED_ATTR_UPDATE_; + + public String getId() { + return id; + } + + public RecordWithInvalidAttributeNameOnRoot setId(String id) { + this.id = id; + return this; + } + + public Instant getAttr_NESTED_ATTR_UPDATE_() { + return attr_NESTED_ATTR_UPDATE_; + } + + public RecordWithInvalidAttributeNameOnRoot setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + this.attr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; + return this; + } +} From 0f3b61a7937127aad743832f945370172ead098e Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Tue, 3 Feb 2026 13:13:23 +0200 Subject: [PATCH 03/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- ...AutoGeneratedTimestampRecordExtension.java | 74 ++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) 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 09e0f403bcea..d3904d3f9712 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 @@ -206,45 +206,67 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex private Map processNestedObject(Map nestedMap, TableSchema nestedSchema, Instant currentInstant) { - Map updatedNestedMap = new HashMap<>(nestedMap); + Map updatedNestedMap = nestedMap; + boolean updated = false; + 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)); + for (String key : customMetadataObject) { + if (!updated) { + updatedNestedMap = new HashMap<>(nestedMap); + updated = true; + } + insertTimestampInItemToTransform(updatedNestedMap, String.valueOf(key), + nestedSchema.converterForAttribute(key), currentInstant); + } } - nestedMap.forEach((nestedKey, nestedValue) -> { + for (Map.Entry entry : nestedMap.entrySet()) { + String nestedKey = entry.getKey(); + AttributeValue nestedValue = entry.getValue(); if (nestedValue.hasM()) { Optional> childSchemaOptional = getNestedSchema(nestedSchema, nestedKey); if (childSchemaOptional.isPresent()) { - updatedNestedMap.put( - nestedKey, - AttributeValue.builder() - .m(processNestedObject(nestedValue.m(), childSchemaOptional.get(), currentInstant)) - .build()); - } else { - // No schema available for this nested object - skip timestamp processing but preserve the object - updatedNestedMap.put(nestedKey, nestedValue); + Map processed = processNestedObject(nestedValue.m(), childSchemaOptional.get(), currentInstant); + if (!Objects.equals(processed, nestedValue.m())) { + if (!updated) { + updatedNestedMap = new HashMap<>(nestedMap); + updated = true; + } + updatedNestedMap.put(nestedKey, AttributeValue.builder().m(processed).build()); + } } + } else if (nestedValue.hasL() && !nestedValue.l().isEmpty()) { + // Check first non-null element to determine if this is a list of maps + AttributeValue firstElement = nestedValue.l().stream() + .filter(Objects::nonNull) + .findFirst() + .orElse(null); - } else if (nestedValue.hasL() && !nestedValue.l().isEmpty() && nestedValue.l().get(0).hasM()) { - TableSchema listElementSchema = getTableSchemaForListElement(nestedSchema, nestedKey); - 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()); + if (firstElement != null && firstElement.hasM()) { + TableSchema listElementSchema = getTableSchemaForListElement(nestedSchema, nestedKey); + List updatedList = nestedValue + .l() + .stream() + .map(listItem -> (listItem != null && listItem.hasM()) ? + AttributeValue.builder() + .m(processNestedObject(listItem.m(), + listElementSchema, + currentInstant)).build() : listItem) + .collect(Collectors.toList()); + if (!updatedList.equals(nestedValue.l())) { + if (!updated) { + updatedNestedMap = new HashMap<>(nestedMap); + updated = true; + } + updatedNestedMap.put(nestedKey, AttributeValue.builder().l(updatedList).build()); + } + } } - }); + } return updatedNestedMap; } From af027ad6cd0905f5da23ec76a594d15dcce4f772 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Tue, 3 Feb 2026 13:27:21 +0200 Subject: [PATCH 04/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- ...AutoGeneratedTimestampRecordExtension.java | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) 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 d3904d3f9712..0e3903d764dc 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 @@ -22,15 +22,14 @@ import java.time.Clock; import java.time.Instant; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; 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; @@ -164,16 +163,19 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex if (firstElement != null && firstElement.hasM()) { TableSchema elementListSchema = getTableSchemaForListElement(context.tableSchema(), key); - List updatedList = - value.l() - .stream() - .map(listItem -> (listItem != null && listItem.hasM()) ? - AttributeValue.builder() - .m(processNestedObject(listItem.m(), - elementListSchema, - currentInstant)) - .build() : listItem) - .collect(Collectors.toList()); + Collection updatedList = new ArrayList<>(value.l().size()); + for (AttributeValue listItem : value.l()) { + if (listItem != null && listItem.hasM()) { + updatedList.add(AttributeValue.builder() + .m(processNestedObject( + listItem.m(), + elementListSchema, + currentInstant)) + .build()); + } else { + updatedList.add(listItem); + } + } updatedItems.put(key, AttributeValue.builder().l(updatedList).build()); } } @@ -248,16 +250,26 @@ private Map processNestedObject(Map listElementSchema = getTableSchemaForListElement(nestedSchema, nestedKey); - List updatedList = nestedValue - .l() - .stream() - .map(listItem -> (listItem != null && listItem.hasM()) ? - AttributeValue.builder() - .m(processNestedObject(listItem.m(), - listElementSchema, - currentInstant)).build() : listItem) - .collect(Collectors.toList()); - if (!updatedList.equals(nestedValue.l())) { + + Collection updatedList = new ArrayList<>(nestedValue.l().size()); + boolean listModified = false; + for (AttributeValue listItem : nestedValue.l()) { + if (listItem != null && listItem.hasM()) { + AttributeValue updatedItem = AttributeValue.builder() + .m(processNestedObject( + listItem.m(), + listElementSchema, + currentInstant)) + .build(); + updatedList.add(updatedItem); + if (!Objects.equals(updatedItem, listItem)) { + listModified = true; + } + } else { + updatedList.add(listItem); + } + } + if (listModified) { if (!updated) { updatedNestedMap = new HashMap<>(nestedMap); updated = true; From 480827d47e2cbfaed550cff5915836eff587d0ae Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Tue, 3 Feb 2026 16:00:58 +0200 Subject: [PATCH 05/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- ...AutoGeneratedTimestampRecordExtension.java | 120 +++++---- .../AutoGeneratedTimestampRecordTest.java | 230 ++++++++++++++++++ 2 files changed, 307 insertions(+), 43 deletions(-) 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 0e3903d764dc..2026bc932211 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 @@ -77,6 +77,13 @@ * {@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. + * + *

+ * Note: This implementation uses a temporary cache keyed by {@link TableSchema} instance. + * When updating timestamps in nested objects or lists, the correct {@code TableSchema} must be used for each object. + * This cache ensures that each nested object is processed with its own schema, avoiding redundant lookups and ensuring + * all annotated timestamp fields are updated correctly. + *

*/ @SdkPublicApi @ThreadSafe @@ -146,11 +153,17 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex Map updatedItems = new HashMap<>(); Instant currentInstant = clock.instant(); + // Use TableSchema instance as the cache key + Map, TableSchema> schemaInstanceCache = new HashMap<>(); + 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); + Optional> nestedSchemaOpt = getNestedSchema(context.tableSchema(), key); + if (nestedSchemaOpt.isPresent()) { + TableSchema nestedSchema = nestedSchemaOpt.get(); + TableSchema cachedSchema = getOrCacheSchema(schemaInstanceCache, nestedSchema); + Map processed = + processNestedObject(value.m(), cachedSchema, currentInstant, schemaInstanceCache); updatedItems.put(key, AttributeValue.builder().m(processed).build()); } } else if (value.hasL() && !value.l().isEmpty()) { @@ -162,21 +175,24 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex if (firstElement != null && firstElement.hasM()) { TableSchema elementListSchema = getTableSchemaForListElement(context.tableSchema(), key); - - Collection updatedList = new ArrayList<>(value.l().size()); - for (AttributeValue listItem : value.l()) { - if (listItem != null && listItem.hasM()) { - updatedList.add(AttributeValue.builder() - .m(processNestedObject( - listItem.m(), - elementListSchema, - currentInstant)) - .build()); - } else { - updatedList.add(listItem); + if (elementListSchema != null) { + TableSchema cachedSchema = getOrCacheSchema(schemaInstanceCache, elementListSchema); + Collection updatedList = new ArrayList<>(value.l().size()); + for (AttributeValue listItem : value.l()) { + if (listItem != null && listItem.hasM()) { + updatedList.add(AttributeValue.builder() + .m(processNestedObject( + listItem.m(), + cachedSchema, + currentInstant, + schemaInstanceCache)) + .build()); + } else { + updatedList.add(listItem); + } } + updatedItems.put(key, AttributeValue.builder().l(updatedList).build()); } - updatedItems.put(key, AttributeValue.builder().l(updatedList).build()); } } }); @@ -206,8 +222,20 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex .build(); } + private static TableSchema getOrCacheSchema( + Map, TableSchema> schemaInstanceCache, TableSchema schema) { + + TableSchema cachedSchema = schemaInstanceCache.get(schema); + if (cachedSchema == null) { + schemaInstanceCache.put(schema, schema); + cachedSchema = schema; + } + return cachedSchema; + } + private Map processNestedObject(Map nestedMap, TableSchema nestedSchema, - Instant currentInstant) { + Instant currentInstant, + Map, TableSchema> schemaInstanceCache) { Map updatedNestedMap = nestedMap; boolean updated = false; @@ -230,9 +258,13 @@ private Map processNestedObject(Map> childSchemaOptional = getNestedSchema(nestedSchema, nestedKey); - if (childSchemaOptional.isPresent()) { - Map processed = processNestedObject(nestedValue.m(), childSchemaOptional.get(), currentInstant); + Optional> childSchemaOpt = getNestedSchema(nestedSchema, nestedKey); + if (childSchemaOpt.isPresent()) { + TableSchema childSchema = childSchemaOpt.get(); + TableSchema cachedSchema = getOrCacheSchema(schemaInstanceCache, childSchema); + Map processed = processNestedObject( + nestedValue.m(), cachedSchema, currentInstant, schemaInstanceCache); + if (!Objects.equals(processed, nestedValue.m())) { if (!updated) { updatedNestedMap = new HashMap<>(nestedMap); @@ -247,34 +279,36 @@ private Map processNestedObject(Map listElementSchema = getTableSchemaForListElement(nestedSchema, nestedKey); - - Collection updatedList = new ArrayList<>(nestedValue.l().size()); - boolean listModified = false; - for (AttributeValue listItem : nestedValue.l()) { - if (listItem != null && listItem.hasM()) { - AttributeValue updatedItem = AttributeValue.builder() - .m(processNestedObject( - listItem.m(), - listElementSchema, - currentInstant)) - .build(); - updatedList.add(updatedItem); - if (!Objects.equals(updatedItem, listItem)) { - listModified = true; + if (listElementSchema != null) { + TableSchema cachedSchema = getOrCacheSchema(schemaInstanceCache, listElementSchema); + Collection updatedList = new ArrayList<>(nestedValue.l().size()); + boolean listModified = false; + for (AttributeValue listItem : nestedValue.l()) { + if (listItem != null && listItem.hasM()) { + AttributeValue updatedItem = AttributeValue.builder() + .m(processNestedObject( + listItem.m(), + cachedSchema, + currentInstant, + schemaInstanceCache)) + .build(); + updatedList.add(updatedItem); + if (!Objects.equals(updatedItem, listItem)) { + listModified = true; + } + } else { + updatedList.add(listItem); } - } else { - updatedList.add(listItem); } - } - if (listModified) { - if (!updated) { - updatedNestedMap = new HashMap<>(nestedMap); - updated = true; + if (listModified) { + if (!updated) { + updatedNestedMap = new HashMap<>(nestedMap); + updated = true; + } + updatedNestedMap.put(nestedKey, AttributeValue.builder().l(updatedList).build()); } - updatedNestedMap.put(nestedKey, AttributeValue.builder().l(updatedList).build()); } } } 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 3047ca927379..312f32fa0af3 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 @@ -17,6 +17,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel2RecordWithList; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel3RecordWithList; @@ -118,6 +124,83 @@ public class AutoGeneratedTimestampRecordTest extends LocalDynamoDbSyncTestBase .setter(Record::setNestedRecord)) .build(); + + private static final TableSchema LEVEL3_SCHEMA = + StaticTableSchema.builder(RecursiveRecord.class) + .newItemSupplier(RecursiveRecord::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(RecursiveRecord::getId) + .setter(RecursiveRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("name") + .getter(RecursiveRecord::getName) + .setter(RecursiveRecord::setName)) + .addAttribute(Instant.class, + a -> a.name("grandchildTimestamp1") + .getter(RecursiveRecord::getChildTimestamp1) + .setter(RecursiveRecord::setChildTimestamp1) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(Instant.class, + a -> a.name("grandchildTimestamp2") + .getter(RecursiveRecord::getChildTimestamp2) + .setter(RecursiveRecord::setChildTimestamp2) + .tags(autoGeneratedTimestampAttribute()) + ).build(); + + private static final TableSchema LEVEL2_SCHEMA = + StaticTableSchema.builder(RecursiveRecord.class) + .newItemSupplier(RecursiveRecord::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(RecursiveRecord::getId) + .setter(RecursiveRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("name") + .getter(RecursiveRecord::getName) + .setter(RecursiveRecord::setName)) + .addAttribute(Instant.class, + a -> a.name("childTimestamp1") + .getter(RecursiveRecord::getChildTimestamp1) + .setter(RecursiveRecord::setChildTimestamp1) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(Instant.class, + a -> a.name("childTimestamp2") + .getter(RecursiveRecord::getChildTimestamp2) + .setter(RecursiveRecord::setChildTimestamp2) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf(RecursiveRecord.class, LEVEL3_SCHEMA), + a -> a.name("child") + .getter(RecursiveRecord::getChild) + .setter(RecursiveRecord::setChild) + ).build(); + + private static final TableSchema LEVEL1_SCHEMA = + StaticTableSchema.builder(RecursiveRecord.class) + .newItemSupplier(RecursiveRecord::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(RecursiveRecord::getId) + .setter(RecursiveRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, + a -> a.name("name") + .getter(RecursiveRecord::getName) + .setter(RecursiveRecord::setName)) + .addAttribute(Instant.class, + a -> a.name("parentTimestamp") + .getter(RecursiveRecord::getParentTimestamp) + .setter(RecursiveRecord::setParentTimestamp) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf(RecursiveRecord.class, LEVEL2_SCHEMA), + a -> a.name("child") + .getter(RecursiveRecord::getChild) + .setter(RecursiveRecord::setChild) + ).build(); + + private final DynamoDbTable mappedTable; private final Clock mockCLock = Mockito.mock(Clock.class); @@ -590,6 +673,58 @@ public void putItemFollowedByUpdatesShouldGenerateTimestampsOnNestedFieldsList() is(MOCKED_INSTANT_UPDATE_TWO.toString())); } + @Test + public void recursiveRecord_schemasWithSameTypeButDifferentAttributes_allTimestampsAreUpdated() { + RecursiveRecord level3 = new RecursiveRecord() + .setId("l3_id") + .setName("l3_name"); + + RecursiveRecord level2 = new RecursiveRecord() + .setId("l2_id") + .setName("l2_name") + .setChild(level3); + + RecursiveRecord level1 = new RecursiveRecord() + .setId("l1_id") + .setName("l1_name") + .setChild(level2); + + String tableName = getConcreteTableName("recursive-record-table-all-timestamps"); + DynamoDbTable parentTable = enhancedClient.table(tableName, LEVEL1_SCHEMA); + parentTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_NOW); + + parentTable.putItem(level1); + + Map key = new HashMap<>(); + key.put("id", AttributeValue.builder().s("l1_id").build()); + GetItemResponse response = getDynamoDbClient().getItem(GetItemRequest.builder() + .tableName(tableName) + .key(key) + .consistentRead(true) + .build()); + Map item = response.item(); + + // Assert parent timestamp is set + assertNotNull(item.get("parentTimestamp")); + assertEquals(MOCKED_INSTANT_NOW.toString(), item.get("parentTimestamp").s()); + + Map childMap = item.get("child").m(); + RecursiveRecord deserializedChild = LEVEL2_SCHEMA.mapToItem(childMap); + assertNotNull(deserializedChild); + // Assert child timestamps are set + assertEquals(MOCKED_INSTANT_NOW, deserializedChild.getChildTimestamp1()); + assertEquals(MOCKED_INSTANT_NOW, deserializedChild.getChildTimestamp2()); + + Map grandchildMap = item.get("child").m(); + RecursiveRecord deserializedGrandchild = LEVEL2_SCHEMA.mapToItem(grandchildMap); + assertNotNull(deserializedGrandchild); + // Assert grandchild timestamps are set + assertEquals(MOCKED_INSTANT_NOW, deserializedGrandchild.getChildTimestamp1()); + assertEquals(MOCKED_INSTANT_NOW, deserializedGrandchild.getChildTimestamp2()); + } + private GetItemResponse getItemAsStoredFromDDB() { Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s("id").build()); @@ -809,5 +944,100 @@ public String toString() { } } + public static class RecursiveRecord { + private String id; + private String name; + private Instant parentTimestamp; + private Instant childTimestamp1; + private Instant childTimestamp2; + private RecursiveRecord child; + + public String getId() { + return id; + } + + public RecursiveRecord setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public RecursiveRecord setName(String name) { + this.name = name; + return this; + } + + public Instant getParentTimestamp() { + return parentTimestamp; + } + + public RecursiveRecord setParentTimestamp(Instant parentTimestamp) { + this.parentTimestamp = parentTimestamp; + return this; + } + + public Instant getChildTimestamp1() { + return childTimestamp1; + } + public RecursiveRecord setChildTimestamp1(Instant childTimestamp1) { + this.childTimestamp1 = childTimestamp1; + return this; + } + + public Instant getChildTimestamp2() { + return childTimestamp2; + } + + public RecursiveRecord setChildTimestamp2(Instant childTimestamp2) { + this.childTimestamp2 = childTimestamp2; + return this; + } + + public RecursiveRecord getChild() { + return child; + } + + public RecursiveRecord setChild(RecursiveRecord child) { + this.child = child; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RecursiveRecord that = (RecursiveRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(name, that.name) && + Objects.equals(parentTimestamp, that.parentTimestamp) && + Objects.equals(childTimestamp1, that.childTimestamp1) && + Objects.equals(childTimestamp2, that.childTimestamp2) && + Objects.equals(child, that.child); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, parentTimestamp, childTimestamp1, childTimestamp2, child); + } + + @Override + public String toString() { + return "RecursiveRecord{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + ", parentTimestamp=" + parentTimestamp + + ", childTimestamp1=" + childTimestamp1 + + ", childTimestamp2=" + childTimestamp2 + + ", child=" + child + + '}'; + } + } } From 3412f529257e55916ccaa44ee99516a5b43bc934 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Tue, 3 Feb 2026 16:01:17 +0200 Subject: [PATCH 06/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- .../functionaltests/AutoGeneratedTimestampRecordTest.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 312f32fa0af3..db9206687a2e 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 @@ -18,21 +18,17 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel2RecordWithList; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel3RecordWithList; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel4Record; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticRecordWithList; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute; import java.time.Clock; import java.time.Instant; @@ -55,8 +51,8 @@ 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.functionaltests.models.RecordWithInvalidAttributeNameOnRoot; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnRoot; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; From cabe751ccfc6bb33f38d7d112118c1ffc6670c78 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 4 Feb 2026 04:38:07 +0200 Subject: [PATCH 07/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- .../AutoGeneratedTimestampRecordTest.java | 1039 ----- .../AutogeneratedTimestampTest.java | 595 --- .../NestedAutoGeneratedTimestampTest.java | 1387 +++++++ .../NestedUpdateBehaviorTest.java | 450 +- .../models/AutogeneratedTimestampModels.java | 3618 ----------------- .../models/NestedStructureTestModels.java | 1105 +++++ 6 files changed, 2727 insertions(+), 5467 deletions(-) delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampRecordTest.java delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutogeneratedTimestampTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedAutoGeneratedTimestampTest.java delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampModels.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedStructureTestModels.java 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 deleted file mode 100644 index db9206687a2e..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampRecordTest.java +++ /dev/null @@ -1,1039 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, AutoTimestamp 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.functionaltests; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel2RecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel3RecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel4Record; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute; -import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; - -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneOffset; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -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.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.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnRoot; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; -import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; -import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; -import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; -import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; -import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; -import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; - -public class AutoGeneratedTimestampRecordTest extends LocalDynamoDbSyncTestBase { - - public static final Instant MOCKED_INSTANT_NOW = Instant.now(Clock.fixed(Instant.parse("2019-01-13T14:00:00Z"), - ZoneOffset.UTC)); - - 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 TableSchema FLATTENED_TABLE_SCHEMA = - StaticTableSchema.builder(FlattenedRecord.class) - .newItemSupplier(FlattenedRecord::new) - .addAttribute(Instant.class, a -> a.name("generated") - .getter(FlattenedRecord::getGenerated) - .setter(FlattenedRecord::setGenerated) - .tags(autoGeneratedTimestampAttribute())) - .build(); - - private static final TableSchema TABLE_SCHEMA = - StaticTableSchema.builder(Record.class) - .newItemSupplier(Record::new) - .addAttribute(String.class, a -> a.name("id") - .getter(Record::getId) - .setter(Record::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attribute") - .getter(Record::getAttribute) - .setter(Record::setAttribute)) - .addAttribute(Instant.class, a -> a.name("lastUpdatedDate") - .getter(Record::getLastUpdatedDate) - .setter(Record::setLastUpdatedDate) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(Instant.class, a -> a.name("createdDate") - .getter(Record::getCreatedDate) - .setter(Record::setCreatedDate) - .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())) - .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(NestedStaticRecordWithList.class, - buildStaticSchemaForNestedRecordWithList(), - b -> b.ignoreNulls(true)), - a -> a.name("nestedRecord").getter(Record::getNestedRecord) - .setter(Record::setNestedRecord)) - .build(); - - - private static final TableSchema LEVEL3_SCHEMA = - StaticTableSchema.builder(RecursiveRecord.class) - .newItemSupplier(RecursiveRecord::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(RecursiveRecord::getId) - .setter(RecursiveRecord::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("name") - .getter(RecursiveRecord::getName) - .setter(RecursiveRecord::setName)) - .addAttribute(Instant.class, - a -> a.name("grandchildTimestamp1") - .getter(RecursiveRecord::getChildTimestamp1) - .setter(RecursiveRecord::setChildTimestamp1) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(Instant.class, - a -> a.name("grandchildTimestamp2") - .getter(RecursiveRecord::getChildTimestamp2) - .setter(RecursiveRecord::setChildTimestamp2) - .tags(autoGeneratedTimestampAttribute()) - ).build(); - - private static final TableSchema LEVEL2_SCHEMA = - StaticTableSchema.builder(RecursiveRecord.class) - .newItemSupplier(RecursiveRecord::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(RecursiveRecord::getId) - .setter(RecursiveRecord::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("name") - .getter(RecursiveRecord::getName) - .setter(RecursiveRecord::setName)) - .addAttribute(Instant.class, - a -> a.name("childTimestamp1") - .getter(RecursiveRecord::getChildTimestamp1) - .setter(RecursiveRecord::setChildTimestamp1) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(Instant.class, - a -> a.name("childTimestamp2") - .getter(RecursiveRecord::getChildTimestamp2) - .setter(RecursiveRecord::setChildTimestamp2) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf(RecursiveRecord.class, LEVEL3_SCHEMA), - a -> a.name("child") - .getter(RecursiveRecord::getChild) - .setter(RecursiveRecord::setChild) - ).build(); - - private static final TableSchema LEVEL1_SCHEMA = - StaticTableSchema.builder(RecursiveRecord.class) - .newItemSupplier(RecursiveRecord::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(RecursiveRecord::getId) - .setter(RecursiveRecord::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("name") - .getter(RecursiveRecord::getName) - .setter(RecursiveRecord::setName)) - .addAttribute(Instant.class, - a -> a.name("parentTimestamp") - .getter(RecursiveRecord::getParentTimestamp) - .setter(RecursiveRecord::setParentTimestamp) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf(RecursiveRecord.class, LEVEL2_SCHEMA), - a -> a.name("child") - .getter(RecursiveRecord::getChild) - .setter(RecursiveRecord::setChild) - ).build(); - - - private final DynamoDbTable mappedTable; - - private final Clock mockCLock = Mockito.mock(Clock.class); - - - private final DynamoDbEnhancedClient enhancedClient = - DynamoDbEnhancedClient.builder() - .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedTimestampRecordExtension.builder().baseClock(mockCLock).build()) - .build(); - private final String concreteTableName; - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - { - concreteTableName = getConcreteTableName("table-name"); - mappedTable = enhancedClient.table(concreteTableName, TABLE_SCHEMA); - } - - public static Record createUniqueFakeItem() { - Record record = new Record(); - record.setId(UUID.randomUUID().toString()); - return record; - } - - @Before - public void createTable() { - Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_NOW); - mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - } - - @After - public void deleteTable() { - getDynamoDbClient().deleteTable(DeleteTableRequest.builder() - .tableName(getConcreteTableName("table-name")) - .build()); - } - - @Test - public void putNewRecord_setsInitialTimestamps_onAllNestedBeanLevels() { - NestedStaticLevel4Record nestedLevel4 = new NestedStaticLevel4Record().setAttr("attrL4"); - NestedStaticLevel3RecordWithList nestedLevel3 = - new NestedStaticLevel3RecordWithList().setAttr("attrL3").setLevel4(nestedLevel4); - NestedStaticLevel2RecordWithList level2 = - new NestedStaticLevel2RecordWithList().setAttr("attrL2").setLevel3(nestedLevel3); - NestedStaticRecordWithList nestedLevel1 = new NestedStaticRecordWithList().setAttr("attrL1").setLevel2(level2); - - 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 stored = getItemAsStoredFromDDB(); - - NestedStaticLevel4Record expL4 = new NestedStaticLevel4Record() - .setTime(MOCKED_INSTANT_NOW).setAttr("attrL4"); - NestedStaticLevel3RecordWithList expL3 = new NestedStaticLevel3RecordWithList() - .setTime(MOCKED_INSTANT_NOW).setAttr("attrL3").setLevel4(expL4); - NestedStaticLevel2RecordWithList expL2 = new NestedStaticLevel2RecordWithList() - .setTime(MOCKED_INSTANT_NOW).setAttr("attrL2").setLevel3(expL3); - NestedStaticRecordWithList expL1 = new NestedStaticRecordWithList() - .setTime(MOCKED_INSTANT_NOW).setAttr("attrL1").setLevel2(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) - .setNestedRecord(expL1); - - assertThat(result, is(expectedRecord)); - 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("time").s(), is(MOCKED_INSTANT_NOW.toString())); - - // level2 assertions - Map lvl2Map = lvl1Map.get("level2").m(); - assertThat(lvl2Map.get("time").s(), is(MOCKED_INSTANT_NOW.toString())); - - // level3 assertions - Map lvl3Map = lvl2Map.get("level3").m(); - assertThat(lvl3Map.get("time").s(), is(MOCKED_INSTANT_NOW.toString())); - - // level4 assertions - Map lvl4Map = lvl3Map.get("level4").m(); - assertThat(lvl4Map.get("time").s(), is(MOCKED_INSTANT_NOW.toString())); - } - - @Test - public void updateNewRecordSetsAutoFormattedDate() { - Record result = mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one") - .setNestedRecord(new NestedStaticRecordWithList() - .setAttr("attribute")))); - GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); - FlattenedRecord flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_NOW); - NestedStaticRecordWithList expectednestedLevel1 = new NestedStaticRecordWithList().setTime(MOCKED_INSTANT_NOW) - .setAttr("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) - .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("time").s(), - is(MOCKED_INSTANT_NOW.toString())); - } - - @Test - public void putExistingRecordUpdatedWithAutoFormattedTimestamps() { - mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one"))); - Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); - 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); - 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")); - - Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); - mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one"))); - result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - itemAsStoredInDDB = getItemAsStoredFromDDB(); - flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_UPDATE_ONE); - expectedRecord = new Record().setId("id") - .setAttribute("one") - .setLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setConvertedLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - // Note : Since we are doing PutItem second time, the createDate gets updated, - .setCreatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_ONE) - .setFlattenedRecord(flattenedRecord); - - System.out.println("result " + result); - assertThat(result, is(expectedRecord)); - // The data in DDB is stored in converted time format - assertThat(itemAsStoredInDDB.item().get("convertedLastUpdatedDate").s(), is("14 01 2019 14:00:00")); - } - - @Test - public void putItemFollowedByUpdates() { - mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one"))); - Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); - 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); - 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")); - - //First Update - Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); - - result = mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one"))); - itemAsStoredInDDB = getItemAsStoredFromDDB(); - flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_UPDATE_ONE); - expectedRecord = new Record().setId("id") - .setAttribute("one") - .setLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setConvertedLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setCreatedDate(MOCKED_INSTANT_NOW) - .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_ONE) - .setFlattenedRecord(flattenedRecord); - assertThat(result, is(expectedRecord)); - // The data in DDB is stored in converted time format - assertThat(itemAsStoredInDDB.item().get("convertedLastUpdatedDate").s(), is("14 01 2019 14:00:00")); - - //Second Update - Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_TWO); - result = mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one"))); - itemAsStoredInDDB = getItemAsStoredFromDDB(); - flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_UPDATE_TWO); - expectedRecord = new Record().setId("id") - .setAttribute("one") - .setLastUpdatedDate(MOCKED_INSTANT_UPDATE_TWO) - .setConvertedLastUpdatedDate(MOCKED_INSTANT_UPDATE_TWO) - .setCreatedDate(MOCKED_INSTANT_NOW) - .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_TWO) - .setFlattenedRecord(flattenedRecord); - assertThat(result, is(expectedRecord)); - // The data in DDB is stored in converted time format - assertThat(itemAsStoredInDDB.item().get("convertedLastUpdatedDate").s(), is("15 01 2019 14:00:00")); - - System.out.println(Instant.ofEpochMilli(Long.parseLong(itemAsStoredInDDB.item().get("lastUpdatedDateInEpochMillis").n()))); - assertThat(Long.parseLong(itemAsStoredInDDB.item().get("lastUpdatedDateInEpochMillis").n()), - is(MOCKED_INSTANT_UPDATE_TWO.toEpochMilli())); - } - - @Test - public void putExistingRecordWithConditionExpressions() { - mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one"))); - Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); - 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); - 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")); - - Expression conditionExpression = Expression.builder() - .expression("#k = :v OR #k = :v1") - .putExpressionName("#k", "attribute") - .putExpressionValue(":v", stringValue("one")) - .putExpressionValue(":v1", stringValue("wrong2")) - .build(); - - Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); - mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) - .item(new Record().setId("id").setAttribute("one")) - .conditionExpression(conditionExpression) - .build()); - - result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_UPDATE_ONE); - expectedRecord = new Record().setId("id") - .setAttribute("one") - .setLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setConvertedLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - // Note that this is a second putItem call so create date is updated. - .setCreatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_ONE) - .setFlattenedRecord(flattenedRecord); - assertThat(result, is(expectedRecord)); - } - - @Test - public void updateExistingRecordWithConditionExpressions() { - mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one"))); - GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); - // The data in DDB is stored in converted time format - assertThat(itemAsStoredInDDB.item().get("convertedLastUpdatedDate").s(), is("13 01 2019 14:00:00")); - Expression conditionExpression = Expression.builder() - .expression("#k = :v OR #k = :v1") - .putExpressionName("#k", "attribute") - .putExpressionValue(":v", stringValue("one")) - .putExpressionValue(":v1", stringValue("wrong2")) - .build(); - - Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); - mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one")) - .conditionExpression(conditionExpression)); - - Record result = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - FlattenedRecord flattenedRecord = new FlattenedRecord().setGenerated(MOCKED_INSTANT_UPDATE_ONE); - Record expectedRecord = new Record().setId("id") - .setAttribute("one") - .setLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setConvertedLastUpdatedDate(MOCKED_INSTANT_UPDATE_ONE) - .setCreatedDate(MOCKED_INSTANT_NOW) - .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_UPDATE_ONE) - .setFlattenedRecord(flattenedRecord); - assertThat(result, is(expectedRecord)); - } - - @Test - public void putItemConditionTestFailure() { - - mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one"))); - - Expression conditionExpression = Expression.builder() - .expression("#k = :v OR #k = :v1") - .putExpressionName("#k", "attribute") - .putExpressionValue(":v", stringValue("wrong1")) - .putExpressionValue(":v1", stringValue("wrong2")) - .build(); - - thrown.expect(ConditionalCheckFailedException.class); - mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) - .item(new Record().setId("id").setAttribute("one")) - .conditionExpression(conditionExpression) - .build()); - - } - - @Test - public void updateItemConditionTestFailure() { - mappedTable.updateItem(r -> r.item(new Record().setId("id").setAttribute("one"))); - Expression conditionExpression = Expression.builder() - .expression("#k = :v OR #k = :v1") - .putExpressionName("#k", "attribute") - .putExpressionValue(":v", stringValue("wrong1")) - .putExpressionValue(":v1", stringValue("wrong2")) - .build(); - thrown.expect(ConditionalCheckFailedException.class); - mappedTable.putItem(PutItemEnhancedRequest.builder(Record.class) - .item(new Record().setId("id").setAttribute("one")) - .conditionExpression(conditionExpression) - .build()); - } - - @Test - public void autogenerateTimestamps_onItemWithNonInstantAttributeName_throwsException() { - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Attribute 'lastUpdatedDate' of Class type class java.lang.String is not a suitable " - + "Java Class type to be used as a Auto Generated Timestamp attribute. Only java.time." - + "Instant Class type is supported."); - StaticTableSchema.builder(RecordWithStringUpdateDate.class) - .newItemSupplier(RecordWithStringUpdateDate::new) - .addAttribute(String.class, a -> a.name("id") - .getter(RecordWithStringUpdateDate::getId) - .setter(RecordWithStringUpdateDate::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("lastUpdatedDate") - .getter(RecordWithStringUpdateDate::getLastUpdatedDate) - .setter(RecordWithStringUpdateDate::setLastUpdatedDate) - .tags(autoGeneratedTimestampAttribute())) - .build(); - } - - @Test - public void autogenerateTimestamps_onItemWithRootAttributeNameContainingReservedMarker_throwsException() { - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Attribute name 'attr_NESTED_ATTR_UPDATE_' contains reserved marker " - + "'_NESTED_ATTR_UPDATE_' and is not allowed."); - - StaticTableSchema - .builder(RecordWithInvalidAttributeNameOnRoot.class) - .newItemSupplier(RecordWithInvalidAttributeNameOnRoot::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(RecordWithInvalidAttributeNameOnRoot::getId) - .setter(RecordWithInvalidAttributeNameOnRoot::setId) - .tags(primaryPartitionKey())) - - .addAttribute(Instant.class, - a -> a.name("attr_NESTED_ATTR_UPDATE_") - .getter(RecordWithInvalidAttributeNameOnRoot::getAttr_NESTED_ATTR_UPDATE_) - .setter(RecordWithInvalidAttributeNameOnRoot::setAttr_NESTED_ATTR_UPDATE_) - .tags(autoGeneratedTimestampAttribute())) - .build(); - } - - @Test - public void autogenerateTimestamps_onItemWithNestedAttributeNameContainingReservedMarker_throwsException() { - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Attribute name 'childAttr_NESTED_ATTR_UPDATE_' contains reserved marker " - + "'_NESTED_ATTR_UPDATE_' and is not allowed."); - - StaticTableSchema - .builder(RecordWithInvalidAttributeNameOnNestedLevel.class) - .newItemSupplier(RecordWithInvalidAttributeNameOnNestedLevel::new) - .addAttribute( - String.class, - a -> a.name("id") - .getter(RecordWithInvalidAttributeNameOnNestedLevel::getId) - .setter(RecordWithInvalidAttributeNameOnNestedLevel::setId) - .tags(primaryPartitionKey())) - - .addAttribute( - EnhancedType.documentOf( - RecordWithReservedMarkerNestedChildAttribute.class, - StaticTableSchema - .builder(RecordWithReservedMarkerNestedChildAttribute.class) - .newItemSupplier(RecordWithReservedMarkerNestedChildAttribute::new) - .addAttribute(Instant.class, - a -> a.name("childAttr_NESTED_ATTR_UPDATE_") - .getter(RecordWithReservedMarkerNestedChildAttribute::getAttr_NESTED_ATTR_UPDATE_) - .setter(RecordWithReservedMarkerNestedChildAttribute::setAttr_NESTED_ATTR_UPDATE_) - .tags(autoGeneratedTimestampAttribute())) - .build()), - - a -> a.name("nestedChildAttribute") - .getter(RecordWithInvalidAttributeNameOnNestedLevel::getNestedChildAttribute) - .setter(RecordWithInvalidAttributeNameOnNestedLevel::setNestedChildAttribute)) - .build(); - } - - @Test - public void putItemFollowedByUpdatesShouldGenerateTimestampsOnNestedFields() { - mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one") - .setNestedRecord(new NestedStaticRecordWithList().setAttr("attribute")))); - mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); - - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute")); - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").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 NestedStaticRecordWithList().setAttr( - "attribute1")))); - itemAsStoredInDDB = getItemAsStoredFromDDB(); - - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute1")); - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").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 NestedStaticRecordWithList().setAttr( - "attribute2")))); - itemAsStoredInDDB = getItemAsStoredFromDDB(); - - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute2")); - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").s(), - is(MOCKED_INSTANT_UPDATE_TWO.toString())); - } - - @Test - public void putItemFollowedByUpdatesShouldGenerateTimestampsOnNestedFieldsList() { - mappedTable.putItem(r -> r.item(new Record().setId("id").setAttribute("one") - .setNestedRecord(new NestedStaticRecordWithList().setAttr("attribute")))); - mappedTable.getItem(r -> r.key(k -> k.partitionValue("id"))); - GetItemResponse itemAsStoredInDDB = getItemAsStoredFromDDB(); - - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute")); - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").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 NestedStaticRecordWithList().setAttr( - "attribute1")))); - itemAsStoredInDDB = getItemAsStoredFromDDB(); - - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute1")); - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").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 NestedStaticRecordWithList().setAttr( - "attribute2")))); - itemAsStoredInDDB = getItemAsStoredFromDDB(); - - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("attr").s(), is("attribute2")); - assertThat(itemAsStoredInDDB.item().get("nestedRecord").m().get("time").s(), - is(MOCKED_INSTANT_UPDATE_TWO.toString())); - } - - @Test - public void recursiveRecord_schemasWithSameTypeButDifferentAttributes_allTimestampsAreUpdated() { - RecursiveRecord level3 = new RecursiveRecord() - .setId("l3_id") - .setName("l3_name"); - - RecursiveRecord level2 = new RecursiveRecord() - .setId("l2_id") - .setName("l2_name") - .setChild(level3); - - RecursiveRecord level1 = new RecursiveRecord() - .setId("l1_id") - .setName("l1_name") - .setChild(level2); - - String tableName = getConcreteTableName("recursive-record-table-all-timestamps"); - DynamoDbTable parentTable = enhancedClient.table(tableName, LEVEL1_SCHEMA); - parentTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - - Mockito.when(mockCLock.instant()).thenReturn(MOCKED_INSTANT_NOW); - - parentTable.putItem(level1); - - Map key = new HashMap<>(); - key.put("id", AttributeValue.builder().s("l1_id").build()); - GetItemResponse response = getDynamoDbClient().getItem(GetItemRequest.builder() - .tableName(tableName) - .key(key) - .consistentRead(true) - .build()); - Map item = response.item(); - - // Assert parent timestamp is set - assertNotNull(item.get("parentTimestamp")); - assertEquals(MOCKED_INSTANT_NOW.toString(), item.get("parentTimestamp").s()); - - Map childMap = item.get("child").m(); - RecursiveRecord deserializedChild = LEVEL2_SCHEMA.mapToItem(childMap); - assertNotNull(deserializedChild); - // Assert child timestamps are set - assertEquals(MOCKED_INSTANT_NOW, deserializedChild.getChildTimestamp1()); - assertEquals(MOCKED_INSTANT_NOW, deserializedChild.getChildTimestamp2()); - - Map grandchildMap = item.get("child").m(); - RecursiveRecord deserializedGrandchild = LEVEL2_SCHEMA.mapToItem(grandchildMap); - assertNotNull(deserializedGrandchild); - // Assert grandchild timestamps are set - assertEquals(MOCKED_INSTANT_NOW, deserializedGrandchild.getChildTimestamp1()); - assertEquals(MOCKED_INSTANT_NOW, deserializedGrandchild.getChildTimestamp2()); - } - - private GetItemResponse getItemAsStoredFromDDB() { - Map key = new HashMap<>(); - key.put("id", AttributeValue.builder().s("id").build()); - return getDynamoDbClient().getItem(GetItemRequest - .builder().tableName(concreteTableName) - .key(key) - .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; - private Instant createdDate; - private Instant lastUpdatedDate; - private Instant convertedLastUpdatedDate; - private Instant lastUpdatedDateInEpochMillis; - private FlattenedRecord flattenedRecord; - private NestedStaticRecordWithList nestedLevel1; - - private String getId() { - return id; - } - - private Record setId(String id) { - this.id = id; - return this; - } - - private String getAttribute() { - return attribute; - } - - private Record setAttribute(String attribute) { - this.attribute = attribute; - return this; - } - - private Instant getLastUpdatedDate() { - return lastUpdatedDate; - } - - private Record setLastUpdatedDate(Instant lastUpdatedDate) { - this.lastUpdatedDate = lastUpdatedDate; - return this; - } - - private Instant getCreatedDate() { - return createdDate; - } - - private Record setCreatedDate(Instant createdDate) { - this.createdDate = createdDate; - return this; - } - - private Instant getConvertedLastUpdatedDate() { - return convertedLastUpdatedDate; - } - - private Record setConvertedLastUpdatedDate(Instant convertedLastUpdatedDate) { - this.convertedLastUpdatedDate = convertedLastUpdatedDate; - return this; - } - - private Instant getLastUpdatedDateInEpochMillis() { - return lastUpdatedDateInEpochMillis; - } - - private Record setLastUpdatedDateInEpochMillis(Instant lastUpdatedDateInEpochMillis) { - this.lastUpdatedDateInEpochMillis = lastUpdatedDateInEpochMillis; - return this; - } - - public FlattenedRecord getFlattenedRecord() { - return flattenedRecord; - } - - public Record setFlattenedRecord(FlattenedRecord flattenedRecord) { - this.flattenedRecord = flattenedRecord; - return this; - } - - public NestedStaticRecordWithList getNestedRecord() { - return nestedLevel1; - } - - public Record setNestedRecord(NestedStaticRecordWithList nestedLevel1) { - this.nestedLevel1 = nestedLevel1; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Record record = (Record) o; - return Objects.equals(id, record.id) && - Objects.equals(attribute, record.attribute) && - Objects.equals(lastUpdatedDate, record.lastUpdatedDate) && - Objects.equals(createdDate, record.createdDate) && - Objects.equals(lastUpdatedDateInEpochMillis, record.lastUpdatedDateInEpochMillis) && - Objects.equals(convertedLastUpdatedDate, record.convertedLastUpdatedDate) && - Objects.equals(flattenedRecord, record.flattenedRecord) && - Objects.equals(nestedLevel1, record.nestedLevel1); - } - - @Override - public int hashCode() { - return Objects.hash(id, attribute, lastUpdatedDate, createdDate, lastUpdatedDateInEpochMillis, - convertedLastUpdatedDate, flattenedRecord, nestedLevel1); - } - - @Override - public String toString() { - return "Record{" + - "id='" + id + '\'' + - ", attribute='" + attribute + '\'' + - ", createdDate=" + createdDate + - ", lastUpdatedDate=" + lastUpdatedDate + - ", convertedLastUpdatedDate=" + convertedLastUpdatedDate + - ", lastUpdatedDateInEpochMillis=" + lastUpdatedDateInEpochMillis + - ", flattenedRecord=" + flattenedRecord + - ", nestedRecord=" + nestedLevel1 + - '}'; - } - } - - private static class RecordWithStringUpdateDate { - private String id; - private String lastUpdatedDate; - - - private String getId() { - return id; - } - - private RecordWithStringUpdateDate setId(String id) { - this.id = id; - return this; - } - - - private String getLastUpdatedDate() { - return lastUpdatedDate; - } - - private RecordWithStringUpdateDate setLastUpdatedDate(String lastUpdatedDate) { - this.lastUpdatedDate = lastUpdatedDate; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RecordWithStringUpdateDate record = (RecordWithStringUpdateDate) o; - return Objects.equals(id, record.id) && - Objects.equals(lastUpdatedDate, record.lastUpdatedDate); - } - - @Override - public int hashCode() { - return Objects.hash(id, lastUpdatedDate); - } - - @Override - public String toString() { - return "RecordWithStringUpdateDate{" + - "id='" + id + '\'' + - ", lastUpdatedDate=" + lastUpdatedDate + - '}'; - } - } - - public static class RecursiveRecord { - private String id; - private String name; - private Instant parentTimestamp; - private Instant childTimestamp1; - private Instant childTimestamp2; - private RecursiveRecord child; - - public String getId() { - return id; - } - - public RecursiveRecord setId(String id) { - this.id = id; - return this; - } - - public String getName() { - return name; - } - - public RecursiveRecord setName(String name) { - this.name = name; - return this; - } - - public Instant getParentTimestamp() { - return parentTimestamp; - } - - public RecursiveRecord setParentTimestamp(Instant parentTimestamp) { - this.parentTimestamp = parentTimestamp; - return this; - } - - public Instant getChildTimestamp1() { - return childTimestamp1; - } - - public RecursiveRecord setChildTimestamp1(Instant childTimestamp1) { - this.childTimestamp1 = childTimestamp1; - return this; - } - - public Instant getChildTimestamp2() { - return childTimestamp2; - } - - public RecursiveRecord setChildTimestamp2(Instant childTimestamp2) { - this.childTimestamp2 = childTimestamp2; - return this; - } - - public RecursiveRecord getChild() { - return child; - } - - public RecursiveRecord setChild(RecursiveRecord child) { - this.child = child; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RecursiveRecord that = (RecursiveRecord) o; - return Objects.equals(id, that.id) && - Objects.equals(name, that.name) && - Objects.equals(parentTimestamp, that.parentTimestamp) && - Objects.equals(childTimestamp1, that.childTimestamp1) && - Objects.equals(childTimestamp2, that.childTimestamp2) && - Objects.equals(child, that.child); - } - - @Override - public int hashCode() { - return Objects.hash(id, name, parentTimestamp, childTimestamp1, childTimestamp2, child); - } - - @Override - public String toString() { - return "RecursiveRecord{" + - "id='" + id + '\'' + - ", name='" + name + '\'' + - ", parentTimestamp=" + parentTimestamp + - ", childTimestamp1=" + childTimestamp1 + - ", childTimestamp2=" + childTimestamp2 + - ", child=" + child + - '}'; - } - } -} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutogeneratedTimestampTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutogeneratedTimestampTest.java deleted file mode 100644 index 1395c7e5bf51..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutogeneratedTimestampTest.java +++ /dev/null @@ -1,595 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.functionaltests; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.hamcrest.CoreMatchers.everyItem; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasProperty; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertNotNull; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_CHILD1; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_CHILD2; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL1; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL2; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL3; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL4; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.CHILD1_KEY; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.CHILD2_KEY; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ID_1; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.LEVEL2_KEY; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableRecordWithMap; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleImmutableRecordWithMap; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.TIME_ATTR; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildBeanSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildBeanSchemaForSimpleRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildImmutableSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildImmutableSchemaForSimpleRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedBeanRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedBeanRecordWithMap; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedImmutableRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedImmutableRecordWithMap; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedStaticRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleBeanRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleBeanRecordWithMap; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleImmutableRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleImmutableRecordWithMap; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleStaticRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticImmutableSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticImmutableSchemaForSimpleRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForSimpleRecordWithList; - -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneOffset; -import java.util.Arrays; -import java.util.Collection; -import java.util.stream.Collectors; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.mockito.Mockito; -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.AutoGeneratedTimestampRecordExtension; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanLevel2RecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanLevel3RecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanLevel4Record; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanRecordWithMap; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableLevel2RecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableLevel3RecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableLevel4Record; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel2RecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel3RecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticLevel4Record; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleBeanRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleBeanRecordWithMap; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleStaticRecordWithList; -import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; - -@RunWith(Parameterized.class) -public class AutogeneratedTimestampTest extends LocalDynamoDbSyncTestBase { - - // Test configuration constants - private static final String TIMESTAMP_STRING = "2019-01-13T14:00:00Z"; - private static final String INVALID_SCHEMA_ERROR = "Invalid table schema"; - - // Table name suffixes - private static final String BASE_TABLE_NAME = "autogen-timestamp-test"; - private static final String SIMPLE_BEAN_TABLE_SUFFIX = "-simple-bean-"; - private static final String NESTED_BEAN_TABLE_SUFFIX = "-nested-bean-"; - private static final String SIMPLE_IMMUTABLE_TABLE_SUFFIX = "-simple-immutable-"; - private static final String NESTED_IMMUTABLE_TABLE_SUFFIX = "-nested-immutable-"; - private static final String SIMPLE_STATIC_TABLE_SUFFIX = "-simple-static-"; - private static final String NESTED_STATIC_TABLE_SUFFIX = "-nested-static-"; - private static final String SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX = "-simple-static-immutable-"; - private static final String NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX = "-nested-static-immutable-"; - - private static final Clock mockClock = Mockito.mock(Clock.class); - private static final Instant MOCKED_INSTANT_NOW = - Instant.now(Clock.fixed(Instant.parse(TIMESTAMP_STRING), ZoneOffset.UTC)); - private DynamoDbEnhancedClient enhancedClient; - private String currentTestTableName; - - private enum RecordLevel {SIMPLE, NESTED} - - private enum SchemaType {BEAN, IMMUTABLE, STATIC, STATIC_IMMUTABLE} - - @Parameterized.Parameters(name = "{0}-{1}") - public static Collection data() { - return Arrays.stream(SchemaType.values()) - .flatMap(schema -> Arrays.stream(RecordLevel.values()) - .map(level -> new Object[] {schema, level})) - .collect(Collectors.toList()); - } - - @Parameterized.Parameter(0) - public SchemaType schemaType; - - @Parameterized.Parameter(1) - public RecordLevel recordLevel; - - @Before - public void beforeClass() { - Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_NOW); - enhancedClient = DynamoDbEnhancedClient.builder() - .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedTimestampRecordExtension.builder() - .baseClock(mockClock) - .build()) - .build(); - } - - @After - public void deleteTable() { - try { - if (currentTestTableName != null) { - getDynamoDbClient().deleteTable(r -> r.tableName(currentTestTableName)); - } - } catch (ResourceNotFoundException e) { - // Table didn't get created, ignore. - } - } - - - @Test - public void shouldPopulateTimestamps_forRecordWithList() { - getTestCaseForRecordWithList(schemaType, recordLevel).run(); - } - - @Test - public void shouldThrowException_forRecordWithSet() { - if (schemaType == SchemaType.BEAN || schemaType == SchemaType.IMMUTABLE) { - assertThatThrownBy(() -> getTestCaseForRecordWithSet(schemaType, recordLevel).run()) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Converter not found for EnhancedType(java.util.Set<"); - } else { - assertThatThrownBy(() -> getTestCaseForRecordWithSet(schemaType, recordLevel).run()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("SetAttributeConverter cannot be created with a parameterized type of '"); - } - } - - @Test - public void shouldThrowException_forRecordWithMap() { - if (schemaType == SchemaType.BEAN || schemaType == SchemaType.IMMUTABLE) { - assertThatThrownBy(() -> getTestCaseForRecordWithMap(schemaType, recordLevel).run()) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("Expected: is <2019-01-13T14:00:00Z>\n but: was null"); - } else { - assertThatThrownBy(() -> getTestCaseForRecordWithMap(schemaType, recordLevel).run()) - .isInstanceOf(RuntimeException.class) - .hasMessageContaining("Converter not found for EnhancedType(java.util.Map { - throw new IllegalArgumentException(INVALID_SCHEMA_ERROR); - }; - } - } - - private Runnable getTestCaseForRecordWithSet(SchemaType schema, RecordLevel level) { - switch (schema) { - case BEAN: - return (level == RecordLevel.SIMPLE) - ? AutogeneratedTimestampModels::buildBeanSchemaForSimpleRecordWithSet - : AutogeneratedTimestampModels::buildBeanSchemaForNestedRecordWithSet; - case IMMUTABLE: - return (level == RecordLevel.SIMPLE) - ? AutogeneratedTimestampModels::buildImmutableSchemaForSimpleRecordWithSet - : AutogeneratedTimestampModels::buildImmutableSchemaForNestedRecordWithSet; - case STATIC: - return (level == RecordLevel.SIMPLE) - ? AutogeneratedTimestampModels::buildStaticSchemaForSimpleRecordWithSet - : AutogeneratedTimestampModels::buildStaticSchemaForNestedRecordWithSet; - case STATIC_IMMUTABLE: - return (level == RecordLevel.SIMPLE) - ? AutogeneratedTimestampModels::buildStaticImmutableSchemaForSimpleRecordWithSet - : AutogeneratedTimestampModels::buildStaticImmutableSchemaForNestedRecordWithSet; - default: - return () -> { - throw new IllegalArgumentException(INVALID_SCHEMA_ERROR); - }; - } - } - - private Runnable getTestCaseForRecordWithMap(SchemaType schema, RecordLevel level) { - switch (schema) { - case BEAN: - return (level == RecordLevel.SIMPLE) - ? this::testAutogeneratedTimestamp_givenBeanSchema_onSimpleRecordWithMap_throwsException - : this::testAutogeneratedTimestamp_givenBeanSchema_onNestedRecordWithMap_throwsException; - case IMMUTABLE: - return (level == RecordLevel.SIMPLE) - ? this::testAutogeneratedTimestamp_givenImmutableSchema_onSimpleRecordWithMap_throwsException - : this::testAutogeneratedTimestamp_givenImmutableSchema_onNestedRecordWithMap_throwsException; - case STATIC: - return (level == RecordLevel.SIMPLE) - ? AutogeneratedTimestampModels::buildStaticSchemaForSimpleRecordWithMap - : AutogeneratedTimestampModels::buildStaticSchemaForNestedRecordWithMap; - case STATIC_IMMUTABLE: - return (level == RecordLevel.SIMPLE) - ? AutogeneratedTimestampModels::buildStaticImmutableSchemaForSimpleRecordWithMap - : AutogeneratedTimestampModels::buildStaticImmutableSchemaForNestedRecordWithMap; - default: - return () -> { - throw new IllegalArgumentException(INVALID_SCHEMA_ERROR); - }; - } - } - - - // Bean table schema + Record with List - private void testAutogeneratedTimestamp_givenBeanSchema_onSimpleRecordWithList_populatesTimestamps() { - TableSchema schema = buildBeanSchemaForSimpleRecordWithList(); - DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, - schema, - buildSimpleBeanRecordWithList()); - - SimpleBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(result.getAttr(), is(ATTR_LEVEL1)); - assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - - assertNotNull(result.getChildList()); - assertThat(result.getChildList().get(0).getAttr(), is(ATTR_CHILD1)); - assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(1).getAttr(), is(ATTR_CHILD2)); - assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); - } - - private void testAutogeneratedTimestamp_givenBeanSchema_onNestedRecordWithList_populatesTimestamps() { - TableSchema schema = buildBeanSchemaForNestedRecordWithList(); - DynamoDbTable table = createAndPut(NESTED_BEAN_TABLE_SUFFIX, - schema, - buildNestedBeanRecordWithList()); - - NestedBeanRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(level1, notNullValue()); - assertThat(level1.getAttr(), is(ATTR_LEVEL1)); - assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2(), notNullValue()); - assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); - assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2List(), notNullValue()); - assertThat(level1.getLevel2List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedBeanLevel2RecordWithList level2 = level1.getLevel2(); - assertThat(level2, notNullValue()); - assertThat(level2.getAttr(), is(ATTR_LEVEL2)); - assertThat(level2.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level2.getLevel3(), notNullValue()); - assertThat(level2.getLevel3().getAttr(), is(ATTR_LEVEL3)); - assertThat(level2.getLevel3().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level2.getLevel3List(), notNullValue()); - assertThat(level2.getLevel3List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedBeanLevel3RecordWithList level3 = level2.getLevel3(); - assertThat(level3, notNullValue()); - assertThat(level3.getAttr(), is(ATTR_LEVEL3)); - assertThat(level3.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level3.getLevel4(), notNullValue()); - assertThat(level3.getLevel4().getAttr(), is(ATTR_LEVEL4)); - assertThat(level3.getLevel4().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level3.getLevel4List(), notNullValue()); - assertThat(level3.getLevel4List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedBeanLevel4Record level4 = level3.getLevel4(); - assertThat(level4, notNullValue()); - assertThat(level4.getAttr(), is(ATTR_LEVEL4)); - assertThat(level4.getTime(), is(MOCKED_INSTANT_NOW)); - } - - // Bean table schema + Record with Map - private void testAutogeneratedTimestamp_givenBeanSchema_onSimpleRecordWithMap_throwsException() { - TableSchema schema = AutogeneratedTimestampModels.buildBeanSchemaForSimpleRecordWithMap(); - DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, - schema, - buildSimpleBeanRecordWithMap()); - - SimpleBeanRecordWithMap result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(result.getAttr(), is(ATTR_LEVEL1)); - assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - - assertNotNull(result.getChildMap()); - assertThat(result.getChildMap().get(CHILD1_KEY).getAttr(), is(ATTR_CHILD1)); - assertThat(result.getChildMap().get(CHILD1_KEY).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildMap().get(CHILD2_KEY).getAttr(), is(ATTR_CHILD2)); - assertThat(result.getChildMap().get(CHILD2_KEY).getTime(), is(MOCKED_INSTANT_NOW)); - } - - private void testAutogeneratedTimestamp_givenBeanSchema_onNestedRecordWithMap_throwsException() { - TableSchema schema = AutogeneratedTimestampModels.buildBeanSchemaForNestedRecordWithMap(); - DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, - schema, - buildNestedBeanRecordWithMap()); - - NestedBeanRecordWithMap level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(level1, notNullValue()); - assertThat(level1.getAttr(), is(ATTR_LEVEL1)); - assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2(), notNullValue()); - assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); - assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); - assertNotNull(level1.getLevel2Map()); - assertThat(level1.getLevel2Map().get(LEVEL2_KEY).getAttr(), is(ATTR_LEVEL2)); - assertThat(level1.getLevel2Map().get(LEVEL2_KEY).getTime(), is(MOCKED_INSTANT_NOW)); - } - - // Immutable table schema + Record with Map - private void testAutogeneratedTimestamp_givenImmutableSchema_onSimpleRecordWithMap_throwsException() { - TableSchema schema = - AutogeneratedTimestampModels.buildImmutableSchemaForSimpleRecordWithMap(); - DynamoDbTable table = createAndPut(SIMPLE_IMMUTABLE_TABLE_SUFFIX, - schema, - buildSimpleImmutableRecordWithMap()); - - SimpleImmutableRecordWithMap result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(result.getAttr(), is(ATTR_LEVEL1)); - assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - - assertNotNull(result.getChildMap()); - assertThat(result.getChildMap().get(CHILD1_KEY).getAttr(), is(ATTR_CHILD1)); - assertThat(result.getChildMap().get(CHILD1_KEY).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildMap().get(CHILD2_KEY).getAttr(), is(ATTR_CHILD2)); - assertThat(result.getChildMap().get(CHILD2_KEY).getTime(), is(MOCKED_INSTANT_NOW)); - } - - private void testAutogeneratedTimestamp_givenImmutableSchema_onNestedRecordWithMap_throwsException() { - TableSchema schema = - AutogeneratedTimestampModels.buildImmutableSchemaForNestedRecordWithMap(); - DynamoDbTable table = createAndPut(NESTED_IMMUTABLE_TABLE_SUFFIX, - schema, - buildNestedImmutableRecordWithMap()); - - NestedImmutableRecordWithMap level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(level1, notNullValue()); - assertThat(level1.getAttr(), is(ATTR_LEVEL1)); - assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2(), notNullValue()); - assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); - assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); - assertNotNull(level1.getLevel2Map()); - assertThat(level1.getLevel2Map().get(LEVEL2_KEY).getAttr(), is(ATTR_LEVEL2)); - assertThat(level1.getLevel2Map().get(LEVEL2_KEY).getTime(), is(MOCKED_INSTANT_NOW)); - } - - // Immutable table schema + Record with List - private void testAutogeneratedTimestamp_givenImmutableSchema_onSimpleRecordWithList_populatesTimestamps() { - TableSchema schema = buildImmutableSchemaForSimpleRecordWithList(); - DynamoDbTable table = createAndPut(SIMPLE_IMMUTABLE_TABLE_SUFFIX, - schema, - buildSimpleImmutableRecordWithList()); - - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(result.getAttr(), is(ATTR_LEVEL1)); - assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - - assertNotNull(result.getChildList()); - assertThat(result.getChildList().get(0).getAttr(), is(ATTR_CHILD1)); - assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(1).getAttr(), is(ATTR_CHILD2)); - assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); - } - - private void testAutogeneratedTimestamp_givenImmutableSchema_onNestedRecordWithList_populatesTimestamps() { - TableSchema schema = buildImmutableSchemaForNestedRecordWithList(); - DynamoDbTable table = createAndPut(NESTED_IMMUTABLE_TABLE_SUFFIX, - schema, - buildNestedImmutableRecordWithList()); - - NestedImmutableRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(level1, notNullValue()); - assertThat(level1.getAttr(), is(ATTR_LEVEL1)); - assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2(), notNullValue()); - assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); - assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2List(), notNullValue()); - assertThat(level1.getLevel2List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedImmutableLevel2RecordWithList level2 = level1.getLevel2(); - assertThat(level2, notNullValue()); - assertThat(level2.getAttr(), is(ATTR_LEVEL2)); - assertThat(level2.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level2.getLevel3(), notNullValue()); - assertThat(level2.getLevel3().getAttr(), is(ATTR_LEVEL3)); - assertThat(level2.getLevel3().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level2.getLevel3List(), notNullValue()); - assertThat(level2.getLevel3List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedImmutableLevel3RecordWithList level3 = level2.getLevel3(); - assertThat(level3, notNullValue()); - assertThat(level3.getAttr(), is(ATTR_LEVEL3)); - assertThat(level3.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level3.getLevel4(), notNullValue()); - assertThat(level3.getLevel4().getAttr(), is(ATTR_LEVEL4)); - assertThat(level3.getLevel4().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level3.getLevel4List(), notNullValue()); - assertThat(level3.getLevel4List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedImmutableLevel4Record level4 = level3.getLevel4(); - assertThat(level4, notNullValue()); - assertThat(level4.getAttr(), is(ATTR_LEVEL4)); - assertThat(level4.getTime(), is(MOCKED_INSTANT_NOW)); - } - - // Static table schema + Record with List - private void testAutogeneratedTimestamp_givenStaticSchema_onSimpleRecordWithList_populatesTimestamps() { - DynamoDbTable table = createAndPut(SIMPLE_STATIC_TABLE_SUFFIX, - buildStaticSchemaForSimpleRecordWithList(), - buildSimpleStaticRecordWithList()); - - SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(result.getAttr(), is(ATTR_LEVEL1)); - assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - - assertNotNull(result.getChildList()); - assertThat(result.getChildList().get(0).getAttr(), is(ATTR_CHILD1)); - assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(1).getAttr(), is(ATTR_CHILD2)); - assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); - } - - private void testAutogeneratedTimestamp_givenStaticImmutableSchema_onSimpleRecordWithList_populatesTimestamps() { - DynamoDbTable table = createAndPut(SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX, - buildStaticImmutableSchemaForSimpleRecordWithList(), - buildSimpleImmutableRecordWithList()); - - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(result.getAttr(), is(ATTR_LEVEL1)); - assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - - assertNotNull(result.getChildList()); - assertThat(result.getChildList().get(0).getAttr(), is(ATTR_CHILD1)); - assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(1).getAttr(), is(ATTR_CHILD2)); - assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); - } - - private void testAutogeneratedTimestamp_givenStaticSchema_onNestedRecordWithList_populatesTimestamps() { - DynamoDbTable table = createAndPut(NESTED_STATIC_TABLE_SUFFIX, - buildStaticSchemaForNestedRecordWithList(), - buildNestedStaticRecordWithList()); - - NestedStaticRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(level1, notNullValue()); - assertThat(level1.getAttr(), is(ATTR_LEVEL1)); - assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2(), notNullValue()); - assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); - assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2List(), notNullValue()); - assertThat(level1.getLevel2List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedStaticLevel2RecordWithList level2 = level1.getLevel2(); - assertThat(level2, notNullValue()); - assertThat(level2.getAttr(), is(ATTR_LEVEL2)); - assertThat(level2.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level2.getLevel3(), notNullValue()); - assertThat(level2.getLevel3().getAttr(), is(ATTR_LEVEL3)); - assertThat(level2.getLevel3().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level2.getLevel3List(), notNullValue()); - assertThat(level2.getLevel3List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedStaticLevel3RecordWithList level3 = level2.getLevel3(); - assertThat(level3, notNullValue()); - assertThat(level3.getAttr(), is(ATTR_LEVEL3)); - assertThat(level3.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level3.getLevel4(), notNullValue()); - assertThat(level3.getLevel4().getAttr(), is(ATTR_LEVEL4)); - assertThat(level3.getLevel4().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level3.getLevel4List(), notNullValue()); - assertThat(level3.getLevel4List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedStaticLevel4Record level4 = level3.getLevel4(); - assertThat(level4, notNullValue()); - assertThat(level4.getAttr(), is(ATTR_LEVEL4)); - assertThat(level4.getTime(), is(MOCKED_INSTANT_NOW)); - } - - private void testAutogeneratedTimestamp_givenStaticImmutableSchema_onNestedRecordWithList_populatesTimestamps() { - DynamoDbTable table = createAndPut(NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX, - buildStaticImmutableSchemaForNestedRecordWithList(), - buildNestedImmutableRecordWithList()); - - NestedImmutableRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - - assertThat(level1, notNullValue()); - assertThat(level1.getAttr(), is(ATTR_LEVEL1)); - assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2(), notNullValue()); - assertThat(level1.getLevel2().getAttr(), is(ATTR_LEVEL2)); - assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2List(), notNullValue()); - assertThat(level1.getLevel2List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedImmutableLevel2RecordWithList level2 = level1.getLevel2(); - assertThat(level2, notNullValue()); - assertThat(level2.getAttr(), is(ATTR_LEVEL2)); - assertThat(level2.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level2.getLevel3(), notNullValue()); - assertThat(level2.getLevel3().getAttr(), is(ATTR_LEVEL3)); - assertThat(level2.getLevel3().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level2.getLevel3List(), notNullValue()); - assertThat(level2.getLevel3List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedImmutableLevel3RecordWithList level3 = level2.getLevel3(); - assertThat(level3, notNullValue()); - assertThat(level3.getAttr(), is(ATTR_LEVEL3)); - assertThat(level3.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level3.getLevel4(), notNullValue()); - assertThat(level3.getLevel4().getAttr(), is(ATTR_LEVEL4)); - assertThat(level3.getLevel4().getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level3.getLevel4List(), notNullValue()); - assertThat(level3.getLevel4List(), everyItem(hasProperty(TIME_ATTR, is(MOCKED_INSTANT_NOW)))); - - NestedImmutableLevel4Record level4 = level3.getLevel4(); - assertThat(level4, notNullValue()); - assertThat(level4.getAttr(), is(ATTR_LEVEL4)); - assertThat(level4.getTime(), is(MOCKED_INSTANT_NOW)); - } - - // Helper for table creation + item insert - private DynamoDbTable createAndPut(String tableSuffix, TableSchema schema, T item) { - currentTestTableName = BASE_TABLE_NAME + tableSuffix + System.nanoTime(); - DynamoDbTable table = enhancedClient.table(currentTestTableName, schema); - table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - table.putItem(item); - return table; - } -} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedAutoGeneratedTimestampTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedAutoGeneratedTimestampTest.java new file mode 100644 index 000000000000..afc3f57afc0f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedAutoGeneratedTimestampTest.java @@ -0,0 +1,1387 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.ATTR_LEVEL1; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.ID_1; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticImmutableSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticImmutableSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +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.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.functionaltests.models.NestedStructureTestModels; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedBeanChild; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedBeanWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleBeanWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleBeanWithMap; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleBeanWithSet; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleImmutableRecordWithMap; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleImmutableRecordWithSet; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnRoot; +import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; + +public class NestedAutoGeneratedTimestampTest extends LocalDynamoDbSyncTestBase { + + private static final Instant MOCKED_INSTANT_NOW = Instant.now(Clock.fixed(Instant.parse("2019-01-13T14:00:00Z"), + ZoneOffset.UTC)); + + private static final Instant MOCKED_INSTANT_UPDATE_ONE = Instant.now(Clock.fixed(Instant.parse("2019-01-14T14:00:00Z"), + ZoneOffset.UTC)); + + private static final Instant MOCKED_INSTANT_UPDATE_TWO = Instant.now(Clock.fixed(Instant.parse("2019-01-15T14:00:00Z"), + ZoneOffset.UTC)); + + private final Clock mockClock = Mockito.mock(Clock.class); + private final DynamoDbEnhancedClient enhancedClient = + DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .extensions(AutoGeneratedTimestampRecordExtension.builder().baseClock(mockClock).build()) + .build(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setup() { + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_NOW); + } + + @After + public void cleanup() { + // Tables are cleaned up by individual tests + } + + @Test + public void putNewRecord_setsInitialTimestamps() { + String tableName = getConcreteTableName("basic-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createBasicRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + BasicRecord item = new BasicRecord() + .setId("id") + .setAttribute("one"); + + table.putItem(r -> r.item(item)); + BasicRecord result = table.getItem(r -> r.key(k -> k.partitionValue("id"))); + + BasicRecord expected = new BasicRecord() + .setId("id") + .setAttribute("one") + .setLastUpdatedDate(MOCKED_INSTANT_NOW) + .setCreatedDate(MOCKED_INSTANT_NOW) + .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_NOW) + .setConvertedLastUpdatedDate(MOCKED_INSTANT_NOW) + .setFlattenedRecord(new FlattenedRecord().setGenerated(MOCKED_INSTANT_NOW)); + + assertThat(result, is(expected)); + + // Verify converted format is stored correctly + GetItemResponse stored = getItemFromDDB(table.tableName(), "id"); + assertThat(stored.item().get("convertedLastUpdatedDate").s(), is("13 01 2019 14:00:00")); + + table.deleteTable(); + } + + @Test + public void updateNewRecord_setsAutoFormattedTimestamps() { + String tableName = getConcreteTableName("basic-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createBasicRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + BasicRecord result = table.updateItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + + BasicRecord expected = new BasicRecord() + .setId("id") + .setAttribute("one") + .setLastUpdatedDate(MOCKED_INSTANT_NOW) + .setCreatedDate(MOCKED_INSTANT_NOW) + .setLastUpdatedDateInEpochMillis(MOCKED_INSTANT_NOW) + .setConvertedLastUpdatedDate(MOCKED_INSTANT_NOW) + .setFlattenedRecord(new FlattenedRecord().setGenerated(MOCKED_INSTANT_NOW)); + + assertThat(result, is(expected)); + + GetItemResponse stored = getItemFromDDB(table.tableName(), "id"); + assertThat(stored.item().get("convertedLastUpdatedDate").s(), is("13 01 2019 14:00:00")); + + table.deleteTable(); + } + + @Test + public void putExistingRecord_updatesTimestamps() { + String tableName = getConcreteTableName("basic-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createBasicRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Initial put + table.putItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + BasicRecord initial = table.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(initial.getCreatedDate(), is(MOCKED_INSTANT_NOW)); + assertThat(initial.getLastUpdatedDate(), is(MOCKED_INSTANT_NOW)); + + // Update with new timestamp + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + table.putItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + + BasicRecord updated = table.getItem(r -> r.key(k -> k.partitionValue("id"))); + // Note: PutItem updates both created and last updated dates + assertThat(updated.getCreatedDate(), is(MOCKED_INSTANT_UPDATE_ONE)); + assertThat(updated.getLastUpdatedDate(), is(MOCKED_INSTANT_UPDATE_ONE)); + + GetItemResponse stored = getItemFromDDB(table.tableName(), "id"); + assertThat(stored.item().get("convertedLastUpdatedDate").s(), is("14 01 2019 14:00:00")); + + table.deleteTable(); + } + + @Test + public void updateExistingRecord_preservesCreatedDate() { + String tableName = getConcreteTableName("basic-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createBasicRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Initial put + table.putItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + + // Update with new timestamp + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + BasicRecord result = table.updateItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + + // UpdateItem preserves created date but updates last updated date + assertThat(result.getCreatedDate(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLastUpdatedDate(), is(MOCKED_INSTANT_UPDATE_ONE)); + + table.deleteTable(); + } + + @Test + public void multipleUpdates_updatesTimestampsCorrectly() { + String tableName = getConcreteTableName("basic-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createBasicRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Initial put + table.putItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + + // First update + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + BasicRecord firstUpdate = table.updateItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + assertThat(firstUpdate.getCreatedDate(), is(MOCKED_INSTANT_NOW)); + assertThat(firstUpdate.getLastUpdatedDate(), is(MOCKED_INSTANT_UPDATE_ONE)); + + // Second update + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_TWO); + BasicRecord secondUpdate = table.updateItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + assertThat(secondUpdate.getCreatedDate(), is(MOCKED_INSTANT_NOW)); + assertThat(secondUpdate.getLastUpdatedDate(), is(MOCKED_INSTANT_UPDATE_TWO)); + + // Verify epoch millis format + GetItemResponse stored = getItemFromDDB(table.tableName(), "id"); + assertThat(Long.parseLong(stored.item().get("lastUpdatedDateInEpochMillis").n()), + is(MOCKED_INSTANT_UPDATE_TWO.toEpochMilli())); + + table.deleteTable(); + } + + @Test + public void putWithConditionExpression_updatesTimestampsWhenConditionMet() { + String tableName = getConcreteTableName("basic-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createBasicRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + table.putItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + + Expression conditionExpression = Expression.builder() + .expression("#k = :v OR #k = :v1") + .putExpressionName("#k", "attribute") + .putExpressionValue(":v", stringValue("one")) + .putExpressionValue(":v1", stringValue("wrong2")) + .build(); + + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + table.putItem(PutItemEnhancedRequest.builder(BasicRecord.class) + .item(new BasicRecord().setId("id").setAttribute("one")) + .conditionExpression(conditionExpression) + .build()); + + BasicRecord result = table.getItem(r -> r.key(k -> k.partitionValue("id"))); + assertThat(result.getLastUpdatedDate(), is(MOCKED_INSTANT_UPDATE_ONE)); + assertThat(result.getCreatedDate(), is(MOCKED_INSTANT_UPDATE_ONE)); // PutItem updates both + + table.deleteTable(); + } + + @Test + public void updateWithConditionExpression_updatesTimestampsWhenConditionMet() { + String tableName = getConcreteTableName("basic-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createBasicRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + table.updateItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + + Expression conditionExpression = Expression.builder() + .expression("#k = :v OR #k = :v1") + .putExpressionName("#k", "attribute") + .putExpressionValue(":v", stringValue("one")) + .putExpressionValue(":v1", stringValue("wrong2")) + .build(); + + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + BasicRecord result = table.updateItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one")) + .conditionExpression(conditionExpression)); + + assertThat(result.getLastUpdatedDate(), is(MOCKED_INSTANT_UPDATE_ONE)); + assertThat(result.getCreatedDate(), is(MOCKED_INSTANT_NOW)); // UpdateItem preserves created date + + table.deleteTable(); + } + + @Test + public void putWithFailedCondition_throwsException() { + String tableName = getConcreteTableName("basic-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createBasicRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + table.putItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + + Expression conditionExpression = Expression.builder() + .expression("#k = :v OR #k = :v1") + .putExpressionName("#k", "attribute") + .putExpressionValue(":v", stringValue("wrong1")) + .putExpressionValue(":v1", stringValue("wrong2")) + .build(); + + thrown.expect(ConditionalCheckFailedException.class); + table.putItem(PutItemEnhancedRequest.builder(BasicRecord.class) + .item(new BasicRecord().setId("id").setAttribute("one")) + .conditionExpression(conditionExpression) + .build()); + + table.deleteTable(); + } + + @Test + public void updateWithFailedCondition_throwsException() { + String tableName = getConcreteTableName("basic-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createBasicRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + table.updateItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one"))); + + Expression conditionExpression = Expression.builder() + .expression("#k = :v OR #k = :v1") + .putExpressionName("#k", "attribute") + .putExpressionValue(":v", stringValue("wrong1")) + .putExpressionValue(":v1", stringValue("wrong2")) + .build(); + + thrown.expect(ConditionalCheckFailedException.class); + table.updateItem(r -> r.item(new BasicRecord().setId("id").setAttribute("one")) + .conditionExpression(conditionExpression)); + + table.deleteTable(); + } + + @Test + public void putNewRecord_setsTimestampsOnAlNestedLevels() { + String tableName = getConcreteTableName("nested-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createNestedRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + NestedBeanChild nestedLevel1 = new NestedBeanChild().setAttr("attrL1"); + + NestedRecord item = new NestedRecord() + .setId("id") + .setAttribute("one") + .setNestedRecord(nestedLevel1); + + table.putItem(r -> r.item(item)); + NestedRecord result = table.getItem(r -> r.key(k -> k.partitionValue("id"))); + + // Verify nested level has timestamp set + assertThat(result.getNestedRecord().getTime(), is(MOCKED_INSTANT_NOW)); + + // Verify in DDB storage + GetItemResponse stored = getItemFromDDB(table.tableName(), "id"); + Map lvl1Map = stored.item().get("nestedRecord").m(); + assertThat(lvl1Map.get("time").s(), is(MOCKED_INSTANT_NOW.toString())); + + table.deleteTable(); + } + + @Test + public void updateNestedRecord_updatesTimestampsOnAllLevels() { + String tableName = getConcreteTableName("nested-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createNestedRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Initial put + table.putItem(r -> r.item(new NestedRecord().setId("id").setAttribute("one") + .setNestedRecord(new NestedBeanChild().setAttr("attribute")))); + + // First update + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + table.updateItem(r -> r.item(new NestedRecord().setId("id").setAttribute("one") + .setNestedRecord(new NestedBeanChild().setAttr("attribute1")))); + + GetItemResponse stored = getItemFromDDB(table.tableName(), "id"); + assertThat(stored.item().get("nestedRecord").m().get("attr").s(), is("attribute1")); + assertThat(stored.item().get("nestedRecord").m().get("time").s(), is(MOCKED_INSTANT_UPDATE_ONE.toString())); + + // Second update + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_TWO); + table.updateItem(r -> r.item(new NestedRecord().setId("id").setAttribute("one") + .setNestedRecord(new NestedBeanChild().setAttr("attribute2")))); + + stored = getItemFromDDB(table.tableName(), "id"); + assertThat(stored.item().get("nestedRecord").m().get("attr").s(), is("attribute2")); + assertThat(stored.item().get("nestedRecord").m().get("time").s(), is(MOCKED_INSTANT_UPDATE_TWO.toString())); + + table.deleteTable(); + } + + @Test + public void recursiveRecord_allTimestampsAreUpdated() { + String tableName = getConcreteTableName("recursive-record-table"); + DynamoDbTable table = enhancedClient.table(tableName, createRecursiveRecordLevel1Schema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + RecursiveRecord level3 = new RecursiveRecord() + .setId("l3_id"); + RecursiveRecord level2 = new RecursiveRecord() + .setId("l2_id") + .setChild(level3); + RecursiveRecord level1 = new RecursiveRecord() + .setId("l1_id") + .setChild(level2); + + table.putItem(level1); + + GetItemResponse response = getItemFromDDB(table.tableName(), "l1_id"); + Map item = response.item(); + + // Assert l1 timestamp is set + assertNotNull(item.get("parentTimestamp")); + assertEquals(MOCKED_INSTANT_NOW.toString(), item.get("parentTimestamp").s()); + + // Assert l2 timestamp is set + Map childMap = item.get("child").m(); + assertNotNull(childMap.get("childTimestamp1")); + assertEquals(MOCKED_INSTANT_NOW.toString(), childMap.get("childTimestamp1").s()); + + // Assert l3 timestamp is set + Map grandchildMap = childMap.get("child").m(); + assertNotNull(grandchildMap.get("grandchildTimestamp1")); + assertEquals(MOCKED_INSTANT_NOW.toString(), grandchildMap.get("grandchildTimestamp1").s()); + + table.deleteTable(); + } + + @Test + public void beanSchema_simpleRecordWithList_populatesTimestamps() { + String tableName = getConcreteTableName("bean-simple-list-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(SimpleBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + table.putItem( + new SimpleBeanWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setChildList(Arrays.asList( + new NestedStructureTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), + new NestedStructureTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2")))); + + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getAttr(), is("attr_level1")); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(0).getAttr(), is("attr_child1")); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(1).getAttr(), is("attr_child2")); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void beanSchema_simpleRecordWithSet_populatesTimestamps() { + String tableName = getConcreteTableName("bean-simple-set-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(SimpleBeanWithSet.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + table.putItem( + new SimpleBeanWithSet() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setChildSet(new HashSet<>(Arrays.asList("child1", "child2")))); + + SimpleBeanWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getAttr(), is("attr_level1")); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildSet(), hasSize(2)); + assertThat(result.getChildSet().contains("child1"), is(true)); + assertThat(result.getChildSet().contains("child2"), is(true)); + + table.deleteTable(); + } + + @Test + public void beanSchema_simpleRecordWithMap_populatesTimestamps() { + String tableName = getConcreteTableName("bean-simple-map-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(SimpleBeanWithMap.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem( + new SimpleBeanWithMap() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setChildMap(new HashMap() {{ + put("child1", "attr_child1"); + put("child2", "attr_child2"); + }})); + + SimpleBeanWithMap result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getAttr(), is("attr_level1")); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildMap().size(), is(2)); + assertThat(result.getChildMap().get("child1"), is("attr_child1")); + assertThat(result.getChildMap().get("child2"), is("attr_child2")); + + table.deleteTable(); + } + + @Test + public void beanSchema_nestedRecordWithList_populatesTimestamps() { + String tableName = getConcreteTableName("bean-nested-list-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(NestedBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem( + new NestedBeanWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setLevel2(new NestedBeanChild().setAttr("attr_level2"))); + + NestedBeanWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(level1.getAttr(), is("attr_level1")); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2().getAttr(), is("attr_level2")); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void immutableSchema_simpleRecordWithList_populatesTimestamps() { + String tableName = getConcreteTableName("immutable-simple-list-table"); + DynamoDbTable table = enhancedClient.table(tableName, + ImmutableTableSchema.create(SimpleImmutableRecordWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem( + SimpleImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .childList(Arrays.asList( + NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + .build()); + + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getAttr(), is("attr_level1")); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(0).getAttr(), is("attr_child1")); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(1).getAttr(), is("attr_child2")); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void immutableSchema_simpleRecordWithSet_populatesTimestamps() { + String tableName = getConcreteTableName("immutable-simple-set-table"); + DynamoDbTable table = enhancedClient.table(tableName, + ImmutableTableSchema.create(SimpleImmutableRecordWithSet.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem( + SimpleImmutableRecordWithSet + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .childSet(new HashSet<>(Arrays.asList("child1", "child2"))) + .build()); + + SimpleImmutableRecordWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getAttr(), is("attr_level1")); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildSet(), hasSize(2)); + assertThat(result.getChildSet().contains("child1"), is(true)); + assertThat(result.getChildSet().contains("child2"), is(true)); + + table.deleteTable(); + } + + @Test + public void immutableSchema_simpleRecordWithMap_populatesTimestamps() { + String tableName = getConcreteTableName("immutable-simple-map-table"); + DynamoDbTable table = enhancedClient.table(tableName, + ImmutableTableSchema.create(SimpleImmutableRecordWithMap.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem( + SimpleImmutableRecordWithMap.builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .childMap(new HashMap() {{ + put("child1", "attr_child1"); + put("child2", "attr_child2"); + }}) + .build()); + + SimpleImmutableRecordWithMap result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getAttr(), is("attr_level1")); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildMap().size(), is(2)); + assertThat(result.getChildMap().get("child1"), is("attr_child1")); + assertThat(result.getChildMap().get("child2"), is("attr_child2")); + + table.deleteTable(); + } + + @Test + public void immutableSchema_nestedRecordWithList_populatesTimestamps() { + String tableName = getConcreteTableName("immutable-nested-list-table"); + DynamoDbTable table = enhancedClient.table(tableName, + ImmutableTableSchema.create(NestedImmutableRecordWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem( + NestedImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList.builder().attr("attr_level2").build()) + .build() + ); + + NestedImmutableRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(level1.getAttr(), is("attr_level1")); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2().getAttr(), is("attr_level2")); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void staticSchema_simpleRecordWithList_populatesTimestamps() { + String tableName = getConcreteTableName("static-simple-list-table"); + TableSchema schema = buildStaticSchemaForSimpleRecordWithList(); + DynamoDbTable table = enhancedClient.table(tableName, schema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + table.putItem( + new SimpleStaticRecordWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1)); + + SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getAttr(), is("attr_level1")); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void staticSchema_nestedRecordWithList_populatesTimestamps() { + String tableName = getConcreteTableName("static-nested-list-table"); + TableSchema schema = buildStaticSchemaForNestedRecordWithList(); + DynamoDbTable table = enhancedClient.table(tableName, schema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + table.putItem( + new NestedStaticRecordWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setLevel2(new NestedStructureTestModels.NestedStaticChildRecordWithList().setAttr("attr_level2"))); + + NestedStaticRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(level1.getAttr(), is("attr_level1")); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2().getAttr(), is("attr_level2")); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void staticImmutableSchema_simpleRecordWithList_populatesTimestamps() { + String tableName = getConcreteTableName("static-immutable-simple-list-table"); + TableSchema schema = buildStaticImmutableSchemaForSimpleRecordWithList(); + DynamoDbTable table = enhancedClient.table(tableName, schema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem( + SimpleImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .childList(Arrays.asList( + NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + .build()); + + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(result.getAttr(), is("attr_level1")); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(0).getAttr(), is("attr_child1")); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(1).getAttr(), is("attr_child2")); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void staticImmutableSchema_nestedRecordWithList_populatesTimestamps() { + String tableName = getConcreteTableName("static-immutable-nested-list-table"); + TableSchema schema = buildStaticImmutableSchemaForNestedRecordWithList(); + DynamoDbTable table = enhancedClient.table(tableName, schema); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + table.putItem( + NestedImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList.builder().attr("attr_level2").build()) + .build()); + + NestedImmutableRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + assertThat(level1.getAttr(), is("attr_level1")); + assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(level1.getLevel2().getAttr(), is("attr_level2")); + assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void autogenerateTimestamps_onNonInstantAttribute_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Attribute 'lastUpdatedDate' of Class type class java.lang.String is not a suitable " + + "Java Class type to be used as a Auto Generated Timestamp attribute. Only java.time." + + "Instant Class type is supported."); + + StaticTableSchema.builder(RecordWithStringUpdateDate.class) + .newItemSupplier(RecordWithStringUpdateDate::new) + .addAttribute(String.class, a -> a.name("id") + .getter(RecordWithStringUpdateDate::getId) + .setter(RecordWithStringUpdateDate::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("lastUpdatedDate") + .getter(RecordWithStringUpdateDate::getLastUpdatedDate) + .setter(RecordWithStringUpdateDate::setLastUpdatedDate) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + @Test + public void autogenerateTimestamps_onRootAttributeWithReservedMarker_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Attribute name 'attr_NESTED_ATTR_UPDATE_' contains reserved marker " + + "'_NESTED_ATTR_UPDATE_' and is not allowed."); + + StaticTableSchema + .builder(RecordWithInvalidAttributeNameOnRoot.class) + .newItemSupplier(RecordWithInvalidAttributeNameOnRoot::new) + .addAttribute(String.class, + a -> a.name("id") + .getter(RecordWithInvalidAttributeNameOnRoot::getId) + .setter(RecordWithInvalidAttributeNameOnRoot::setId) + .tags(primaryPartitionKey())) + .addAttribute(Instant.class, + a -> a.name("attr_NESTED_ATTR_UPDATE_") + .getter(RecordWithInvalidAttributeNameOnRoot::getAttr_NESTED_ATTR_UPDATE_) + .setter(RecordWithInvalidAttributeNameOnRoot::setAttr_NESTED_ATTR_UPDATE_) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + @Test + public void autogenerateTimestamps_onNestedAttributeWithReservedMarker_throwsException() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Attribute name 'childAttr_NESTED_ATTR_UPDATE_' contains reserved marker " + + "'_NESTED_ATTR_UPDATE_' and is not allowed."); + + StaticTableSchema + .builder(RecordWithInvalidAttributeNameOnNestedLevel.class) + .newItemSupplier(RecordWithInvalidAttributeNameOnNestedLevel::new) + .addAttribute( + String.class, + a -> a.name("id") + .getter(RecordWithInvalidAttributeNameOnNestedLevel::getId) + .setter(RecordWithInvalidAttributeNameOnNestedLevel::setId) + .tags(primaryPartitionKey())) + .addAttribute( + EnhancedType.documentOf( + RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute.class, + StaticTableSchema + .builder(RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute.class) + .newItemSupplier(RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute::new) + .addAttribute(Instant.class, + a -> a.name("childAttr_NESTED_ATTR_UPDATE_") + .getter(RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute::getAttr_NESTED_ATTR_UPDATE_) + .setter(RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute::setAttr_NESTED_ATTR_UPDATE_) + .tags(autoGeneratedTimestampAttribute())) + .build()), + a -> a.name("nestedChildAttribute") + .getter(RecordWithInvalidAttributeNameOnNestedLevel::getNestedChildAttribute) + .setter(RecordWithInvalidAttributeNameOnNestedLevel::setNestedChildAttribute)) + .build(); + } + + private GetItemResponse getItemFromDDB(String tableName, String id) { + Map key = new HashMap<>(); + key.put("id", AttributeValue.builder().s(id).build()); + return getDynamoDbClient().getItem(GetItemRequest.builder() + .tableName(tableName) + .key(key) + .consistentRead(true) + .build()); + } + + + /** + * Basic record class for testing simple timestamp operations with multiple timestamp fields, different converters, and + * flattened record structure. + */ + private static class BasicRecord { + private String id; + private String attribute; + private Instant createdDate; + private Instant lastUpdatedDate; + private Instant convertedLastUpdatedDate; + private Instant lastUpdatedDateInEpochMillis; + private FlattenedRecord flattenedRecord; + + public String getId() { + return id; + } + + public BasicRecord setId(String id) { + this.id = id; + return this; + } + + public String getAttribute() { + return attribute; + } + + public BasicRecord setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + public Instant getLastUpdatedDate() { + return lastUpdatedDate; + } + + public BasicRecord setLastUpdatedDate(Instant lastUpdatedDate) { + this.lastUpdatedDate = lastUpdatedDate; + return this; + } + + public Instant getCreatedDate() { + return createdDate; + } + + public BasicRecord setCreatedDate(Instant createdDate) { + this.createdDate = createdDate; + return this; + } + + public Instant getConvertedLastUpdatedDate() { + return convertedLastUpdatedDate; + } + + public BasicRecord setConvertedLastUpdatedDate(Instant convertedLastUpdatedDate) { + this.convertedLastUpdatedDate = convertedLastUpdatedDate; + return this; + } + + public Instant getLastUpdatedDateInEpochMillis() { + return lastUpdatedDateInEpochMillis; + } + + public BasicRecord setLastUpdatedDateInEpochMillis(Instant lastUpdatedDateInEpochMillis) { + this.lastUpdatedDateInEpochMillis = lastUpdatedDateInEpochMillis; + return this; + } + + public FlattenedRecord getFlattenedRecord() { + return flattenedRecord; + } + + public BasicRecord setFlattenedRecord(FlattenedRecord flattenedRecord) { + this.flattenedRecord = flattenedRecord; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BasicRecord that = (BasicRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(attribute, that.attribute) && + Objects.equals(lastUpdatedDate, that.lastUpdatedDate) && + Objects.equals(createdDate, that.createdDate) && + Objects.equals(lastUpdatedDateInEpochMillis, that.lastUpdatedDateInEpochMillis) && + Objects.equals(convertedLastUpdatedDate, that.convertedLastUpdatedDate) && + Objects.equals(flattenedRecord, that.flattenedRecord); + } + + @Override + public int hashCode() { + return Objects.hash(id, attribute, lastUpdatedDate, createdDate, lastUpdatedDateInEpochMillis, + convertedLastUpdatedDate, flattenedRecord); + } + } + + /** + * Flattened record class for testing flattening functionality with auto-generated timestamps. + */ + 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); + } + } + + /** + * Nested record class for testing nested timestamp operations with multiple timestamp fields and nested structure using + * shared NestedBeanChild. + */ + private static class NestedRecord { + private String id; + private String attribute; + private Instant lastUpdatedDate; + private Instant createdDate; + private Instant lastUpdatedDateInEpochMillis; + private Instant convertedLastUpdatedDate; + private FlattenedRecord flattenedRecord; + private NestedBeanChild nestedRecord; + + public String getId() { + return id; + } + + public NestedRecord setId(String id) { + this.id = id; + return this; + } + + public String getAttribute() { + return attribute; + } + + public NestedRecord setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + public Instant getLastUpdatedDate() { + return lastUpdatedDate; + } + + public NestedRecord setLastUpdatedDate(Instant lastUpdatedDate) { + this.lastUpdatedDate = lastUpdatedDate; + return this; + } + + public Instant getCreatedDate() { + return createdDate; + } + + public NestedRecord setCreatedDate(Instant createdDate) { + this.createdDate = createdDate; + return this; + } + + public Instant getConvertedLastUpdatedDate() { + return convertedLastUpdatedDate; + } + + public NestedRecord setConvertedLastUpdatedDate(Instant convertedLastUpdatedDate) { + this.convertedLastUpdatedDate = convertedLastUpdatedDate; + return this; + } + + public Instant getLastUpdatedDateInEpochMillis() { + return lastUpdatedDateInEpochMillis; + } + + public NestedRecord setLastUpdatedDateInEpochMillis(Instant lastUpdatedDateInEpochMillis) { + this.lastUpdatedDateInEpochMillis = lastUpdatedDateInEpochMillis; + return this; + } + + public FlattenedRecord getFlattenedRecord() { + return flattenedRecord; + } + + public NestedRecord setFlattenedRecord(FlattenedRecord flattenedRecord) { + this.flattenedRecord = flattenedRecord; + return this; + } + + public NestedBeanChild getNestedRecord() { + return nestedRecord; + } + + public NestedRecord setNestedRecord(NestedBeanChild nestedRecord) { + this.nestedRecord = nestedRecord; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedRecord that = (NestedRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(attribute, that.attribute) && + Objects.equals(lastUpdatedDate, that.lastUpdatedDate) && + Objects.equals(createdDate, that.createdDate) && + Objects.equals(lastUpdatedDateInEpochMillis, that.lastUpdatedDateInEpochMillis) && + Objects.equals(convertedLastUpdatedDate, that.convertedLastUpdatedDate) && + Objects.equals(flattenedRecord, that.flattenedRecord) && + Objects.equals(nestedRecord, that.nestedRecord); + } + + @Override + public int hashCode() { + return Objects.hash(id, attribute, lastUpdatedDate, createdDate, lastUpdatedDateInEpochMillis, + convertedLastUpdatedDate, flattenedRecord, nestedRecord); + } + } + + /** + * Recursive record class for testing recursive timestamp operations with multiple timestamp fields at different nesting + * levels. + */ + private static class RecursiveRecord { + private String id; + private String name; + private Instant parentTimestamp; + private Instant childTimestamp1; + private Instant childTimestamp2; + private RecursiveRecord child; + + public String getId() { + return id; + } + + public RecursiveRecord setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public RecursiveRecord setName(String name) { + this.name = name; + return this; + } + + public Instant getParentTimestamp() { + return parentTimestamp; + } + + public RecursiveRecord setParentTimestamp(Instant parentTimestamp) { + this.parentTimestamp = parentTimestamp; + return this; + } + + public Instant getChildTimestamp1() { + return childTimestamp1; + } + + public RecursiveRecord setChildTimestamp1(Instant childTimestamp1) { + this.childTimestamp1 = childTimestamp1; + return this; + } + + public Instant getChildTimestamp2() { + return childTimestamp2; + } + + public RecursiveRecord setChildTimestamp2(Instant childTimestamp2) { + this.childTimestamp2 = childTimestamp2; + return this; + } + + public RecursiveRecord getChild() { + return child; + } + + public RecursiveRecord setChild(RecursiveRecord child) { + this.child = child; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RecursiveRecord that = (RecursiveRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(name, that.name) && + Objects.equals(parentTimestamp, that.parentTimestamp) && + Objects.equals(childTimestamp1, that.childTimestamp1) && + Objects.equals(childTimestamp2, that.childTimestamp2) && + Objects.equals(child, that.child); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, parentTimestamp, childTimestamp1, childTimestamp2, child); + } + } + + /** + * Record class for validation tests to ensure non-Instant types throw appropriate exceptions. + */ + private static class RecordWithStringUpdateDate { + private String id; + private String lastUpdatedDate; + + public String getId() { + return id; + } + + public RecordWithStringUpdateDate setId(String id) { + this.id = id; + return this; + } + + public String getLastUpdatedDate() { + return lastUpdatedDate; + } + + public RecordWithStringUpdateDate setLastUpdatedDate(String lastUpdatedDate) { + this.lastUpdatedDate = lastUpdatedDate; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RecordWithStringUpdateDate that = (RecordWithStringUpdateDate) o; + return Objects.equals(id, that.id) && Objects.equals(lastUpdatedDate, that.lastUpdatedDate); + } + + @Override + public int hashCode() { + return Objects.hash(id, lastUpdatedDate); + } + } + + /** + * Creates a table schema for FlattenedRecord with auto-generated timestamp attribute. + * + * @return TableSchema for FlattenedRecord + */ + private TableSchema createFlattenedRecordSchema() { + return StaticTableSchema.builder(FlattenedRecord.class) + .newItemSupplier(FlattenedRecord::new) + .addAttribute(Instant.class, a -> a.name("generated") + .getter(FlattenedRecord::getGenerated) + .setter(FlattenedRecord::setGenerated) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + /** + * Creates a comprehensive table schema for BasicRecord with multiple timestamp fields, different converters, and flattened + * record structure. + * + * @return TableSchema for BasicRecord + */ + private TableSchema createBasicRecordSchema() { + TableSchema flattenedSchema = createFlattenedRecordSchema(); + + return StaticTableSchema.builder(BasicRecord.class) + .newItemSupplier(BasicRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(BasicRecord::getId) + .setter(BasicRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attribute") + .getter(BasicRecord::getAttribute) + .setter(BasicRecord::setAttribute)) + .addAttribute(Instant.class, a -> a.name("lastUpdatedDate") + .getter(BasicRecord::getLastUpdatedDate) + .setter(BasicRecord::setLastUpdatedDate) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(Instant.class, a -> a.name("createdDate") + .getter(BasicRecord::getCreatedDate) + .setter(BasicRecord::setCreatedDate) + .tags(autoGeneratedTimestampAttribute(), + updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, a -> a.name("lastUpdatedDateInEpochMillis") + .getter(BasicRecord::getLastUpdatedDateInEpochMillis) + .setter(BasicRecord::setLastUpdatedDateInEpochMillis) + .attributeConverter(EpochMillisFormatTestConverter.create()) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(Instant.class, a -> a.name("convertedLastUpdatedDate") + .getter(BasicRecord::getConvertedLastUpdatedDate) + .setter(BasicRecord::setConvertedLastUpdatedDate) + .attributeConverter(TimeFormatUpdateTestConverter.create()) + .tags(autoGeneratedTimestampAttribute())) + .flatten(flattenedSchema, BasicRecord::getFlattenedRecord, BasicRecord::setFlattenedRecord) + .build(); + } + + /** + * Creates a table schema for NestedRecord with multiple timestamp fields and nested structure. + * + * @return TableSchema for NestedRecord + */ + private TableSchema createNestedRecordSchema() { + TableSchema flattenedSchema = createFlattenedRecordSchema(); + + return StaticTableSchema.builder(NestedRecord.class) + .newItemSupplier(NestedRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(NestedRecord::getId) + .setter(NestedRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attribute") + .getter(NestedRecord::getAttribute) + .setter(NestedRecord::setAttribute)) + .addAttribute(Instant.class, a -> a.name("lastUpdatedDate") + .getter(NestedRecord::getLastUpdatedDate) + .setter(NestedRecord::setLastUpdatedDate) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(Instant.class, a -> a.name("createdDate") + .getter(NestedRecord::getCreatedDate) + .setter(NestedRecord::setCreatedDate) + .tags(autoGeneratedTimestampAttribute(), + updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, a -> a.name("lastUpdatedDateInEpochMillis") + .getter(NestedRecord::getLastUpdatedDateInEpochMillis) + .setter(NestedRecord::setLastUpdatedDateInEpochMillis) + .attributeConverter(EpochMillisFormatTestConverter.create()) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(Instant.class, a -> a.name("convertedLastUpdatedDate") + .getter(NestedRecord::getConvertedLastUpdatedDate) + .setter(NestedRecord::setConvertedLastUpdatedDate) + .attributeConverter(TimeFormatUpdateTestConverter.create()) + .tags(autoGeneratedTimestampAttribute())) + .flatten(flattenedSchema, NestedRecord::getFlattenedRecord, NestedRecord::setFlattenedRecord) + .addAttribute(EnhancedType.documentOf(NestedBeanChild.class, + BeanTableSchema.create(NestedBeanChild.class), + b -> b.ignoreNulls(true)), + a -> a.name("nestedRecord") + .getter(NestedRecord::getNestedRecord) + .setter(NestedRecord::setNestedRecord)) + .build(); + } + + /** + * Creates a table schema for the deepest level (Level 3) of recursive records. + * + * @return TableSchema for RecursiveRecord Level 3 + */ + private TableSchema createRecursiveRecordLevel3Schema() { + return StaticTableSchema.builder(RecursiveRecord.class) + .newItemSupplier(RecursiveRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(RecursiveRecord::getId) + .setter(RecursiveRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("name") + .getter(RecursiveRecord::getName) + .setter(RecursiveRecord::setName)) + .addAttribute(Instant.class, a -> a.name("grandchildTimestamp1") + .getter(RecursiveRecord::getChildTimestamp1) + .setter(RecursiveRecord::setChildTimestamp1) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(Instant.class, a -> a.name("grandchildTimestamp2") + .getter(RecursiveRecord::getChildTimestamp2) + .setter(RecursiveRecord::setChildTimestamp2) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + /** + * Creates a table schema for the middle level (Level 2) of recursive records. + * + * @return TableSchema for RecursiveRecord Level 2 + */ + private TableSchema createRecursiveRecordLevel2Schema() { + TableSchema level3Schema = createRecursiveRecordLevel3Schema(); + + return StaticTableSchema.builder(RecursiveRecord.class) + .newItemSupplier(RecursiveRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(RecursiveRecord::getId) + .setter(RecursiveRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("name") + .getter(RecursiveRecord::getName) + .setter(RecursiveRecord::setName)) + .addAttribute(Instant.class, a -> a.name("childTimestamp1") + .getter(RecursiveRecord::getChildTimestamp1) + .setter(RecursiveRecord::setChildTimestamp1) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(Instant.class, a -> a.name("childTimestamp2") + .getter(RecursiveRecord::getChildTimestamp2) + .setter(RecursiveRecord::setChildTimestamp2) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf(RecursiveRecord.class, level3Schema), + a -> a.name("child") + .getter(RecursiveRecord::getChild) + .setter(RecursiveRecord::setChild)) + .build(); + } + + /** + * Creates a table schema for the top level (Level 1) of recursive records. + * + * @return TableSchema for RecursiveRecord Level 1 + */ + private TableSchema createRecursiveRecordLevel1Schema() { + TableSchema level2Schema = createRecursiveRecordLevel2Schema(); + + return StaticTableSchema.builder(RecursiveRecord.class) + .newItemSupplier(RecursiveRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(RecursiveRecord::getId) + .setter(RecursiveRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("name") + .getter(RecursiveRecord::getName) + .setter(RecursiveRecord::setName)) + .addAttribute(Instant.class, a -> a.name("parentTimestamp") + .getter(RecursiveRecord::getParentTimestamp) + .setter(RecursiveRecord::setParentTimestamp) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf(RecursiveRecord.class, level2Schema), + a -> a.name("child") + .getter(RecursiveRecord::getChild) + .setter(RecursiveRecord::setChild)) + .build(); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java index 17cccd6ccae0..49d5b0d360f0 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java @@ -16,86 +16,48 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ATTR_LEVEL1; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.ID_1; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildBeanSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildBeanSchemaForSimpleRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildImmutableSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildImmutableSchemaForSimpleRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedBeanRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedImmutableRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildNestedStaticRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleBeanRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleImmutableRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildSimpleStaticRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticImmutableSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticImmutableSchemaForSimpleRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.buildStaticSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.ATTR_LEVEL1; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.ID_1; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedStaticChildRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticImmutableSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticImmutableSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticSchemaForSimpleRecordWithList; import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; import java.util.Arrays; -import java.util.Collection; -import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.mockito.Mockito; 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.AutoGeneratedTimestampRecordExtension; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedBeanRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.NestedStaticRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleBeanRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampModels.SimpleStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedBeanWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleBeanWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.IgnoreNullsMode; import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; -@RunWith(Parameterized.class) public class NestedUpdateBehaviorTest extends LocalDynamoDbSyncTestBase { private static final String TIMESTAMP_STRING = "2019-01-13T14:00:00Z"; private static final String BASE_TABLE_NAME = "update-behavior-test"; - private static final String SIMPLE_BEAN_TABLE_SUFFIX = "-simple-bean-"; - private static final String NESTED_BEAN_TABLE_SUFFIX = "-nested-bean-"; - private static final String SIMPLE_IMMUTABLE_TABLE_SUFFIX = "-simple-immutable-"; - private static final String NESTED_IMMUTABLE_TABLE_SUFFIX = "-nested-immutable-"; - private static final String SIMPLE_STATIC_TABLE_SUFFIX = "-simple-static-"; - private static final String NESTED_STATIC_TABLE_SUFFIX = "-nested-static-"; - private static final String SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX = "-simple-static-immutable-"; - private static final String NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX = "-nested-static-immutable-"; - private static final Clock mockClock = Mockito.mock(Clock.class); - private static final Instant MOCKED_INSTANT_NOW = - Instant.now(Clock.fixed(Instant.parse(TIMESTAMP_STRING), ZoneOffset.UTC)); + private static final Instant MOCKED_INSTANT_NOW = Instant.now(Clock.fixed(Instant.parse(TIMESTAMP_STRING), ZoneOffset.UTC)); + private DynamoDbEnhancedClient enhancedClient; private String currentTestTableName; - private enum RecordLevel {SIMPLE, NESTED} - - private enum SchemaType {BEAN, IMMUTABLE, STATIC, STATIC_IMMUTABLE} - - @Parameterized.Parameters(name = "{0}-{1}") - public static Collection data() { - return Arrays.stream(SchemaType.values()) - .flatMap(schema -> Arrays.stream(RecordLevel.values()) - .map(level -> new Object[] {schema, level})) - .collect(Collectors.toList()); - } - - @Parameterized.Parameter(0) - public SchemaType schemaType; - - @Parameterized.Parameter(1) - public RecordLevel recordLevel; - @Before public void beforeClass() { Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_NOW); @@ -119,46 +81,19 @@ public void deleteTable() { } @Test - public void updateBehavior_writeIfNotExists_isRespectedOnNestedObjects() { - switch (schemaType) { - case BEAN: - if (recordLevel == RecordLevel.SIMPLE) { - testSimpleBeanUpdateBehavior(); - } else { - testNestedBeanUpdateBehavior(); - } - break; - case IMMUTABLE: - if (recordLevel == RecordLevel.SIMPLE) { - testSimpleImmutableUpdateBehavior(); - } else { - testNestedImmutableUpdateBehavior(); - } - break; - case STATIC: - if (recordLevel == RecordLevel.SIMPLE) { - testSimpleStaticUpdateBehavior(); - } else { - testNestedStaticUpdateBehavior(); - } - break; - case STATIC_IMMUTABLE: - if (recordLevel == RecordLevel.SIMPLE) { - testSimpleStaticImmutableUpdateBehavior(); - } else { - testNestedStaticImmutableUpdateBehavior(); - } - break; - } - } - - private void testSimpleBeanUpdateBehavior() { - TableSchema schema = buildBeanSchemaForSimpleRecordWithList(); - DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, - schema, - buildSimpleBeanRecordWithList()); - - SimpleBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + public void beanSchema_simpleRecord_writeIfNotExists_isRespected() { + TableSchema schema = BeanTableSchema.create(SimpleBeanWithList.class); + DynamoDbTable table = + createAndPut("simple-bean-write-if-not-exists", + schema, + new SimpleBeanWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setChildList(Arrays.asList( + new NestedStructureTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), + new NestedStructureTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2")))); + + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); assertThat(result.getTime()).isEqualTo(MOCKED_INSTANT_NOW); assertThat(result.getChildList()).hasSize(2); @@ -167,45 +102,55 @@ private void testSimpleBeanUpdateBehavior() { result.setAttr("updated_level1"); table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - SimpleBeanRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + SimpleBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change assertThat(updated.getTime()).isNotNull().isEqualTo(MOCKED_INSTANT_NOW); // timestamp should update - } - private void testNestedBeanUpdateBehavior() { - TableSchema schema = buildBeanSchemaForNestedRecordWithList(); - DynamoDbTable table = createAndPut(NESTED_BEAN_TABLE_SUFFIX, - schema, - buildNestedBeanRecordWithList()); + table.deleteTable(); + } - NestedBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + @Test + public void beanSchema_nestedRecord_writeIfNotExists_isRespected() { + TableSchema schema = BeanTableSchema.create(NestedBeanWithList.class); + DynamoDbTable table = + createAndPut("nested-bean-write-if-not-exists", + schema, + new NestedBeanWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setLevel2(new NestedStructureTestModels.NestedBeanChild().setAttr("attr_level2"))); + + NestedBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); assertThat(result.getLevel2()).isNotNull(); assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); - assertThat(result.getLevel2().getLevel3()).isNotNull(); - assertThat(result.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); - assertThat(result.getLevel2().getLevel3().getLevel4()).isNotNull(); - assertThat(result.getLevel2().getLevel3().getLevel4().getAttr()).isEqualTo("attr_level4"); // update with new attr values - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) result.setAttr("updated_level1"); result.getLevel2().setAttr("updated_level2"); - result.getLevel2().getLevel3().setAttr("updated_level3"); - result.getLevel2().getLevel3().getLevel4().setAttr("updated_level4"); table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - NestedBeanRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); + NestedBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change assertThat(updated.getLevel2().getAttr()).isEqualTo("attr_level2"); // should NOT change - assertThat(updated.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); // should NOT change - assertThat(updated.getLevel2().getLevel3().getLevel4().getAttr()).isEqualTo("attr_level4"); // should NOT change + + table.deleteTable(); } - private void testSimpleImmutableUpdateBehavior() { - TableSchema schema = buildImmutableSchemaForSimpleRecordWithList(); - DynamoDbTable table = createAndPut(SIMPLE_IMMUTABLE_TABLE_SUFFIX, - schema, - buildSimpleImmutableRecordWithList()); + @Test + public void immutableSchema_simpleRecord_writeIfNotExists_isRespected() { + TableSchema schema = ImmutableTableSchema.create(SimpleImmutableRecordWithList.class); + DynamoDbTable table = + createAndPut("simple-immutable-write-if-not-exists", + schema, + SimpleImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .childList(Arrays.asList( + NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + .build()); SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); @@ -220,20 +165,28 @@ private void testSimpleImmutableUpdateBehavior() { SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + + table.deleteTable(); } - private void testNestedImmutableUpdateBehavior() { - TableSchema schema = buildImmutableSchemaForNestedRecordWithList(); - DynamoDbTable table = createAndPut(NESTED_IMMUTABLE_TABLE_SUFFIX, - schema, - buildNestedImmutableRecordWithList()); + @Test + public void immutableSchema_nestedRecord_writeIfNotExists_isRespected() { + TableSchema schema = ImmutableTableSchema.create(NestedImmutableRecordWithList.class); + DynamoDbTable table = + createAndPut("nested-immutable-write-if-not-exists", + schema, + NestedImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList + .builder().attr("attr_level2").build()) + .build()); NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); assertThat(result.getLevel2()).isNotNull(); assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); - assertThat(result.getLevel2().getLevel3()).isNotNull(); - assertThat(result.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() @@ -243,12 +196,18 @@ private void testNestedImmutableUpdateBehavior() { NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + + table.deleteTable(); } - private void testSimpleStaticUpdateBehavior() { - DynamoDbTable table = createAndPut(SIMPLE_STATIC_TABLE_SUFFIX, - buildStaticSchemaForSimpleRecordWithList(), - buildSimpleStaticRecordWithList()); + @Test + public void staticSchema_simpleRecord_writeIfNotExists_isRespected() { + DynamoDbTable table = + createAndPut("simple-static-write-if-not-exists", + buildStaticSchemaForSimpleRecordWithList(), + new SimpleStaticRecordWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1)); SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); @@ -260,36 +219,51 @@ private void testSimpleStaticUpdateBehavior() { SimpleStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + + table.deleteTable(); } - private void testNestedStaticUpdateBehavior() { - DynamoDbTable table = createAndPut(NESTED_STATIC_TABLE_SUFFIX, - buildStaticSchemaForNestedRecordWithList(), - buildNestedStaticRecordWithList()); + @Test + public void staticSchema_nestedRecord_writeIfNotExists_isRespected() { + DynamoDbTable table = + createAndPut( + "nested-static-write-if-not-exists", + buildStaticSchemaForNestedRecordWithList(), + new NestedStaticRecordWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setLevel2(new NestedStaticChildRecordWithList().setAttr("attr_level2"))); NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); assertThat(result.getLevel2()).isNotNull(); assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); - assertThat(result.getLevel2().getLevel3()).isNotNull(); - assertThat(result.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) result.setAttr("updated_level1"); result.getLevel2().setAttr("updated_level2"); - result.getLevel2().getLevel3().setAttr("updated_level3"); table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); NestedStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change assertThat(updated.getLevel2().getAttr()).isEqualTo("attr_level2"); // should NOT change - assertThat(updated.getLevel2().getLevel3().getAttr()).isEqualTo("attr_level3"); // should NOT change + + table.deleteTable(); } - private void testSimpleStaticImmutableUpdateBehavior() { - DynamoDbTable table = createAndPut(SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX, - buildStaticImmutableSchemaForSimpleRecordWithList(), - buildSimpleImmutableRecordWithList()); + @Test + public void staticImmutableSchema_simpleRecord_writeIfNotExists_isRespected() { + DynamoDbTable table = + createAndPut("simple-static-immutable-write-if-not-exists", + buildStaticImmutableSchemaForSimpleRecordWithList(), + SimpleImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .childList(Arrays.asList( + NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + .build()); SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); @@ -303,12 +277,22 @@ private void testSimpleStaticImmutableUpdateBehavior() { SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + + table.deleteTable(); } - private void testNestedStaticImmutableUpdateBehavior() { - DynamoDbTable table = createAndPut(NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX, - buildStaticImmutableSchemaForNestedRecordWithList(), - buildNestedImmutableRecordWithList()); + @Test + public void staticImmutableSchema_nestedRecord_writeIfNotExists_isRespected() { + DynamoDbTable table = + createAndPut("nested-static-immutable-write-if-not-exists", + buildStaticImmutableSchemaForNestedRecordWithList(), + NestedImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList + .builder().attr("attr_level2").build()) + .build()); NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); @@ -323,83 +307,74 @@ private void testNestedStaticImmutableUpdateBehavior() { NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change - } - @Test - public void updateBehavior_writeAlways_updatesValuesOnNestedObjects() { - // WRITE_ALWAYS is the default behavior, so we test that id field (without annotation) updates correctly - switch (schemaType) { - case BEAN: - if (recordLevel == RecordLevel.SIMPLE) { - testSimpleBeanWriteAlways(); - } else { - testNestedBeanWriteAlways(); - } - break; - case IMMUTABLE: - if (recordLevel == RecordLevel.SIMPLE) { - testSimpleImmutableWriteAlways(); - } else { - testNestedImmutableWriteAlways(); - } - break; - case STATIC: - if (recordLevel == RecordLevel.SIMPLE) { - testSimpleStaticWriteAlways(); - } else { - testNestedStaticWriteAlways(); - } - break; - case STATIC_IMMUTABLE: - if (recordLevel == RecordLevel.SIMPLE) { - testSimpleStaticImmutableWriteAlways(); - } else { - testNestedStaticImmutableWriteAlways(); - } - break; - } + table.deleteTable(); } - private void testSimpleBeanWriteAlways() { - TableSchema schema = buildBeanSchemaForSimpleRecordWithList(); - SimpleBeanRecordWithList initial = buildSimpleBeanRecordWithList(); + @Test + public void beanSchema_simpleRecord_writeAlways_updatesValues() { + TableSchema schema = BeanTableSchema.create(SimpleBeanWithList.class); + SimpleBeanWithList initial = new SimpleBeanWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setChildList(Arrays.asList( + new NestedStructureTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), + new NestedStructureTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2"))); initial.setId("initial_id"); - DynamoDbTable table = createAndPut(SIMPLE_BEAN_TABLE_SUFFIX, schema, initial); + DynamoDbTable table = createAndPut("simple-bean-write-always", schema, initial); - SimpleBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("initial_id"))); + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("initial_id"))); assertThat(result.getId()).isEqualTo("initial_id"); // update id (no annotation, defaults to WRITE_ALWAYS) - should change result.setId("updated_id"); table.updateItem(result); - SimpleBeanRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + SimpleBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); assertThat(updated).isNotNull(); assertThat(updated.getId()).isEqualTo("updated_id"); + + table.deleteTable(); } - private void testNestedBeanWriteAlways() { - TableSchema schema = buildBeanSchemaForNestedRecordWithList(); - NestedBeanRecordWithList initial = buildNestedBeanRecordWithList(); + @Test + public void beanSchema_nestedRecord_writeAlways_updatesValues() { + TableSchema schema = BeanTableSchema.create(NestedBeanWithList.class); + NestedBeanWithList initial = new NestedBeanWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setLevel2(new NestedStructureTestModels.NestedBeanChild().setAttr("attr_level2")); initial.setId("initial_id"); - DynamoDbTable table = createAndPut(NESTED_BEAN_TABLE_SUFFIX, schema, initial); + DynamoDbTable table = createAndPut("nested-bean-write-always", schema, initial); - NestedBeanRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("initial_id"))); + NestedBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("initial_id"))); assertThat(result.getId()).isEqualTo("initial_id"); // update id - should change (WRITE_ALWAYS is default) result.setId("updated_id"); table.updateItem(result); - NestedBeanRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); + NestedBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); assertThat(updated).isNotNull(); assertThat(updated.getId()).isEqualTo("updated_id"); + + table.deleteTable(); } - private void testSimpleImmutableWriteAlways() { - TableSchema schema = buildImmutableSchemaForSimpleRecordWithList(); - SimpleImmutableRecordWithList initial = buildSimpleImmutableRecordWithList(); - DynamoDbTable table = createAndPut(SIMPLE_IMMUTABLE_TABLE_SUFFIX, schema, initial); + @Test + public void immutableSchema_simpleRecord_writeAlways_updatesValues() { + TableSchema schema = ImmutableTableSchema.create(SimpleImmutableRecordWithList.class); + SimpleImmutableRecordWithList initial = + SimpleImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .childList(Arrays.asList( + NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + .build(); + + DynamoDbTable table = createAndPut("simple-immutable-write-always", schema, initial); SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getId()).isEqualTo(ID_1); @@ -413,12 +388,22 @@ private void testSimpleImmutableWriteAlways() { SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); assertThat(afterUpdate).isNotNull(); assertThat(afterUpdate.getId()).isEqualTo("updated_id"); + + table.deleteTable(); } - private void testNestedImmutableWriteAlways() { - TableSchema schema = buildImmutableSchemaForNestedRecordWithList(); - NestedImmutableRecordWithList initial = buildNestedImmutableRecordWithList(); - DynamoDbTable table = createAndPut(NESTED_IMMUTABLE_TABLE_SUFFIX, schema, initial); + @Test + public void immutableSchema_nestedRecord_writeAlways_updatesValues() { + TableSchema schema = ImmutableTableSchema.create(NestedImmutableRecordWithList.class); + NestedImmutableRecordWithList initial = + NestedImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList + .builder().attr("attr_level2").build()) + .build(); + DynamoDbTable table = createAndPut("nested-immutable-write-always", schema, initial); NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getId()).isEqualTo(ID_1); @@ -432,12 +417,18 @@ private void testNestedImmutableWriteAlways() { NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); assertThat(afterUpdate).isNotNull(); assertThat(afterUpdate.getId()).isEqualTo("updated_id"); + + table.deleteTable(); } - private void testSimpleStaticWriteAlways() { - DynamoDbTable table = createAndPut(SIMPLE_STATIC_TABLE_SUFFIX, - buildStaticSchemaForSimpleRecordWithList(), - buildSimpleStaticRecordWithList()); + @Test + public void staticSchema_simpleRecord_writeAlways_updatesValues() { + DynamoDbTable table = + createAndPut("simple-static-write-always", + buildStaticSchemaForSimpleRecordWithList(), + new SimpleStaticRecordWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1)); SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getId()).isEqualTo(ID_1); @@ -449,12 +440,19 @@ private void testSimpleStaticWriteAlways() { SimpleStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); assertThat(updated).isNotNull(); assertThat(updated.getId()).isEqualTo("updated_id"); + + table.deleteTable(); } - private void testNestedStaticWriteAlways() { - DynamoDbTable table = createAndPut(NESTED_STATIC_TABLE_SUFFIX, - buildStaticSchemaForNestedRecordWithList(), - buildNestedStaticRecordWithList()); + @Test + public void staticSchema_nestedRecord_writeAlways_updatesValues() { + DynamoDbTable table = createAndPut( + "nested-static-write-always", + buildStaticSchemaForNestedRecordWithList(), + new NestedStaticRecordWithList() + .setId(ID_1) + .setAttr(ATTR_LEVEL1) + .setLevel2(new NestedStructureTestModels.NestedStaticChildRecordWithList().setAttr("attr_level2"))); NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getId()).isEqualTo(ID_1); @@ -466,12 +464,23 @@ private void testNestedStaticWriteAlways() { NestedStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); assertThat(updated).isNotNull(); assertThat(updated.getId()).isEqualTo("updated_id"); + + table.deleteTable(); } - private void testSimpleStaticImmutableWriteAlways() { - DynamoDbTable table = createAndPut(SIMPLE_STATIC_IMMUTABLE_TABLE_SUFFIX, - buildStaticImmutableSchemaForSimpleRecordWithList(), - buildSimpleImmutableRecordWithList()); + @Test + public void staticImmutableSchema_simpleRecord_writeAlways_updatesValues() { + DynamoDbTable table = + createAndPut("simple-static-immutable-write-always", + buildStaticImmutableSchemaForSimpleRecordWithList(), + SimpleImmutableRecordWithList + .builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .childList(Arrays.asList( + NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + .build()); SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getId()).isEqualTo(ID_1); @@ -485,12 +494,21 @@ private void testSimpleStaticImmutableWriteAlways() { SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); assertThat(afterUpdate).isNotNull(); assertThat(afterUpdate.getId()).isEqualTo("updated_id"); + + table.deleteTable(); } - private void testNestedStaticImmutableWriteAlways() { - DynamoDbTable table = createAndPut(NESTED_STATIC_IMMUTABLE_TABLE_SUFFIX, - buildStaticImmutableSchemaForNestedRecordWithList(), - buildNestedImmutableRecordWithList()); + @Test + public void staticImmutableSchema_nestedRecord_writeAlways_updatesValues() { + DynamoDbTable table = + createAndPut("nested-static-immutable-write-always", + buildStaticImmutableSchemaForNestedRecordWithList(), + NestedImmutableRecordWithList.builder() + .id(ID_1) + .attr(ATTR_LEVEL1) + .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList + .builder().attr("attr_level2").build()) + .build()); NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); assertThat(result.getId()).isEqualTo(ID_1); @@ -504,6 +522,8 @@ private void testNestedStaticImmutableWriteAlways() { NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); assertThat(afterUpdate).isNotNull(); assertThat(afterUpdate.getId()).isEqualTo("updated_id"); + + table.deleteTable(); } private DynamoDbTable createAndPut(String tableSuffix, TableSchema schema, T item) { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampModels.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampModels.java deleted file mode 100644 index a164036211f3..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampModels.java +++ /dev/null @@ -1,3618 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; - -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; -import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; -import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; -import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; -import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; -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.DynamoDbImmutable; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; -import software.amazon.awssdk.utils.ImmutableMap; - -public final class AutogeneratedTimestampModels { - - private AutogeneratedTimestampModels() { - } - - // Constants - public static final String ID_1 = "1"; - public static final String ID_2 = "2"; - public static final String ID_ATTR = "id"; - public static final String TIME_ATTR = "time"; - - public static final String ATTR_LEVEL1 = "attr_level1"; - public static final String ATTR_LEVEL2 = "attr_level2"; - public static final String ATTR_LEVEL3 = "attr_level3"; - public static final String ATTR_LEVEL4 = "attr_level4"; - public static final String ATTR_CHILD1 = "attr_child1"; - public static final String ATTR_CHILD2 = "attr_child2"; - - public static final String CHILD1_KEY = "child1"; - public static final String CHILD2_KEY = "child2"; - public static final String LEVEL2_KEY = "level2"; - public static final String LEVEL3_KEY = "level3"; - public static final String LEVEL4_KEY = "level4"; - - - // Simple Bean Records - @DynamoDbBean - public static class SimpleBeanRecordWithList { - private String id; - private String attr; - private Instant time; - private List childList; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public SimpleBeanRecordWithList setId(String id) { - this.id = id; - return this; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public SimpleBeanRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public SimpleBeanRecordWithList setTime(Instant time) { - this.time = time; - return this; - } - - - public List getChildList() { - return childList == null ? null : Collections.unmodifiableList(childList); - } - - public SimpleBeanRecordWithList setChildList(List childList) { - this.childList = childList; - return this; - } - } - - @DynamoDbBean - public static class SimpleBeanRecordWithSet { - private String id; - private String attr; - private Instant time; - private Set childSet; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public SimpleBeanRecordWithSet setId(String id) { - this.id = id; - return this; - } - - public String getAttr() { - return attr; - } - - public SimpleBeanRecordWithSet setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public SimpleBeanRecordWithSet setTime(Instant time) { - this.time = time; - return this; - } - - public Set getChildSet() { - return childSet == null ? null : Collections.unmodifiableSet(childSet); - } - - public SimpleBeanRecordWithSet setChildSet(Set childSet) { - this.childSet = childSet; - return this; - } - } - - @DynamoDbBean - public static class SimpleBeanRecordWithMap { - private String id; - private String attr; - private Instant time; - private Map childMap; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public SimpleBeanRecordWithMap setId(String id) { - this.id = id; - return this; - } - - public String getAttr() { - return attr; - } - - public SimpleBeanRecordWithMap setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public SimpleBeanRecordWithMap setTime(Instant time) { - this.time = time; - return this; - } - - public Map getChildMap() { - return childMap == null ? null : Collections.unmodifiableMap(childMap); - } - - public SimpleBeanRecordWithMap setChildMap(Map childMap) { - this.childMap = childMap; - return this; - } - } - - // simple record used by list/set/map as the deepest nested level - @DynamoDbBean - public static class SimpleBeanChild { - private String id; - private String attr; - private Instant time; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public SimpleBeanChild setId(String id) { - this.id = id; - return this; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public SimpleBeanChild setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public SimpleBeanChild setTime(Instant time) { - this.time = time; - return this; - } - } - - - // Nested Bean Records - @DynamoDbBean - public static class NestedBeanRecordWithList { - private String id; - private String attr; - private Instant time; - private NestedBeanLevel2RecordWithList level2; - private List level2List; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public NestedBeanRecordWithList setId(String id) { - this.id = id; - return this; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public NestedBeanRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanRecordWithList setTime(Instant time) { - this.time = time; - return this; - } - - public NestedBeanLevel2RecordWithList getLevel2() { - return level2; - } - - public NestedBeanRecordWithList setLevel2(NestedBeanLevel2RecordWithList level2) { - this.level2 = level2; - return this; - } - - public List getLevel2List() { - return level2List == null ? null : Collections.unmodifiableList(level2List); - } - - public NestedBeanRecordWithList setLevel2List(List level2List) { - this.level2List = level2List; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedBeanRecordWithList)) { - return false; - } - - NestedBeanRecordWithList that = (NestedBeanRecordWithList) o; - return Objects.equals(id, that.id) && - Objects.equals(attr, that.attr) && - Objects.equals(time, that.time) && - Objects.equals(level2, that.level2) && - Objects.equals(level2List, that.level2List); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(id); - result = 31 * result + Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level2); - result = 31 * result + Objects.hashCode(level2List); - return result; - } - } - - @DynamoDbBean - public static class NestedBeanLevel2RecordWithList { - private String attr; - private Instant time; - private NestedBeanLevel3RecordWithList level3; - private List level3List; - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public NestedBeanLevel2RecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanLevel2RecordWithList setTime(Instant time) { - this.time = time; - return this; - } - - public NestedBeanLevel3RecordWithList getLevel3() { - return level3; - } - - public NestedBeanLevel2RecordWithList setLevel3(NestedBeanLevel3RecordWithList level3) { - this.level3 = level3; - return this; - } - - public List getLevel3List() { - return level3List; - } - - public NestedBeanLevel2RecordWithList setLevel3List(List level3List) { - this.level3List = level3List; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedBeanLevel2RecordWithList)) { - return false; - } - - NestedBeanLevel2RecordWithList that = (NestedBeanLevel2RecordWithList) o; - return Objects.equals(attr, that.attr) && - Objects.equals(time, that.time) && - Objects.equals(level3, that.level3) && - Objects.equals(level3List, that.level3List); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level3); - result = 31 * result + Objects.hashCode(level3List); - return result; - } - } - - @DynamoDbBean - public static class NestedBeanLevel3RecordWithList { - private String attr; - private Instant time; - private NestedBeanLevel4Record level4; - private List level4List; - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public NestedBeanLevel3RecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanLevel3RecordWithList setTime(Instant time) { - this.time = time; - return this; - } - - public NestedBeanLevel4Record getLevel4() { - return level4; - } - - public NestedBeanLevel3RecordWithList setLevel4(NestedBeanLevel4Record level4) { - this.level4 = level4; - return this; - } - - public List getLevel4List() { - return level4List; - } - - public NestedBeanLevel3RecordWithList setLevel4List(List level4List) { - this.level4List = level4List; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedBeanLevel3RecordWithList)) { - return false; - } - - NestedBeanLevel3RecordWithList that = (NestedBeanLevel3RecordWithList) o; - return Objects.equals(attr, that.attr) && - Objects.equals(time, that.time) && - Objects.equals(level4, that.level4) && - Objects.equals(level4List, that.level4List); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level4); - result = 31 * result + Objects.hashCode(level4List); - return result; - } - } - - @DynamoDbBean - public static class NestedBeanRecordWithSet { - private String id; - private String attr; - private Instant time; - private NestedBeanLevel2RecordWithSet level2; - private Set level2Set; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public NestedBeanRecordWithSet setId(String v) { - id = v; - return this; - } - - public String getAttr() { - return attr; - } - - public NestedBeanRecordWithSet setAttr(String v) { - attr = v; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanRecordWithSet setTime(Instant v) { - time = v; - return this; - } - - public NestedBeanLevel2RecordWithSet getLevel2() { - return level2; - } - - public NestedBeanRecordWithSet setLevel2(NestedBeanLevel2RecordWithSet v) { - level2 = v; - return this; - } - - public Set getLevel2Set() { - return level2Set; - } - - public NestedBeanRecordWithSet setLevel2Set(Set v) { - level2Set = v; - return this; - } - } - - @DynamoDbBean - public static class NestedBeanLevel2RecordWithSet { - private String attr; - private Instant time; - private NestedBeanLevel3RecordWithSet level3; - private Set level3Set; - - public String getAttr() { - return attr; - } - - public NestedBeanLevel2RecordWithSet setAttr(String v) { - attr = v; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanLevel2RecordWithSet setTime(Instant v) { - time = v; - return this; - } - - public NestedBeanLevel3RecordWithSet getLevel3() { - return level3; - } - - public NestedBeanLevel2RecordWithSet setLevel3(NestedBeanLevel3RecordWithSet v) { - level3 = v; - return this; - } - - public Set getLevel3Set() { - return level3Set; - } - - public NestedBeanLevel2RecordWithSet setLevel3Set(Set v) { - level3Set = v; - return this; - } - } - - @DynamoDbBean - public static class NestedBeanLevel3RecordWithSet { - private String attr; - private Instant time; - private NestedBeanLevel4Record level4; - private Set level4Set; - - public String getAttr() { - return attr; - } - - public NestedBeanLevel3RecordWithSet setAttr(String v) { - attr = v; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanLevel3RecordWithSet setTime(Instant v) { - time = v; - return this; - } - - public NestedBeanLevel4Record getLevel4() { - return level4; - } - - public NestedBeanLevel3RecordWithSet setLevel4(NestedBeanLevel4Record v) { - level4 = v; - return this; - } - - public Set getLevel4Set() { - return level4Set; - } - - public NestedBeanLevel3RecordWithSet setLevel4Set(Set v) { - level4Set = v; - return this; - } - } - - @DynamoDbBean - public static class NestedBeanRecordWithMap { - private String id; - private String attr; - private Instant time; - private NestedBeanLevel2RecordWithMap level2; - private Map level2Map; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public NestedBeanRecordWithMap setId(String id) { - this.id = id; - return this; - } - - public String getAttr() { - return attr; - } - - public NestedBeanRecordWithMap setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanRecordWithMap setTime(Instant time) { - this.time = time; - return this; - } - - public NestedBeanLevel2RecordWithMap getLevel2() { - return level2; - } - - public NestedBeanRecordWithMap setLevel2(NestedBeanLevel2RecordWithMap level2) { - this.level2 = level2; - return this; - } - - public Map getLevel2Map() { - return Collections.unmodifiableMap(level2Map); - } - - public NestedBeanRecordWithMap setLevel2Map(Map level2Map) { - this.level2Map = level2Map; - return this; - } - } - - @DynamoDbBean - public static class NestedBeanLevel2RecordWithMap { - private String attr; - private Instant time; - private NestedBeanLevel3RecordWithMap level3; - private Map level3Map; - - public String getAttr() { - return attr; - } - - public NestedBeanLevel2RecordWithMap setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanLevel2RecordWithMap setTime(Instant time) { - this.time = time; - return this; - } - - public NestedBeanLevel3RecordWithMap getLevel3() { - return level3; - } - - public NestedBeanLevel2RecordWithMap setLevel3(NestedBeanLevel3RecordWithMap level3) { - this.level3 = level3; - return this; - } - - public Map getLevel3Map() { - return Collections.unmodifiableMap(level3Map); - } - - public NestedBeanLevel2RecordWithMap setLevel3Map(Map level3Map) { - this.level3Map = level3Map; - return this; - } - } - - @DynamoDbBean - public static class NestedBeanLevel3RecordWithMap { - private String attr; - private Instant time; - private NestedBeanLevel4Record level4; - private Map level4Map; - - public String getAttr() { - return attr; - } - - public NestedBeanLevel3RecordWithMap setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanLevel3RecordWithMap setTime(Instant time) { - this.time = time; - return this; - } - - public NestedBeanLevel4Record getLevel4() { - return level4; - } - - public NestedBeanLevel3RecordWithMap setLevel4(NestedBeanLevel4Record level4) { - this.level4 = level4; - return this; - } - - public Map getLevel4Map() { - return Collections.unmodifiableMap(level4Map); - } - - public NestedBeanLevel3RecordWithMap setLevel4Map(Map level4Map) { - this.level4Map = level4Map; - return this; - } - } - - // nested record used by list/set/map as the deepest nested level - @DynamoDbBean - public static class NestedBeanLevel4Record { - private String id; - private String attr; - private Instant time; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public NestedBeanLevel4Record setId(String id) { - this.id = id; - return this; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public NestedBeanLevel4Record setAttr(String attr) { - this.attr = attr; - return this; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedBeanLevel4Record setTime(Instant time) { - this.time = time; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedBeanLevel4Record)) { - return false; - } - - NestedBeanLevel4Record that = (NestedBeanLevel4Record) o; - return Objects.equals(attr, that.attr) && - Objects.equals(time, that.time); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - return result; - } - } - - - // Simple Immutable Records - @DynamoDbImmutable(builder = SimpleImmutableRecordWithList.Builder.class) - public static final class SimpleImmutableRecordWithList { - private final String id; - private final String attr; - private final Instant time; - private final List childList; - - private SimpleImmutableRecordWithList(Builder b) { - this.id = b.id; - this.attr = b.attr; - this.time = b.time; - this.childList = b.childList; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - - public List getChildList() { - return childList == null ? null : Collections.unmodifiableList(childList); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private Instant time; - private List childList; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder time(Instant time) { - this.time = time; - return this; - } - - public Builder childList(List childList) { - this.childList = childList; - return this; - } - - public SimpleImmutableRecordWithList build() { - return new SimpleImmutableRecordWithList(this); - } - } - } - - @DynamoDbImmutable(builder = SimpleImmutableRecordWithSet.Builder.class) - public static class SimpleImmutableRecordWithSet { - private final String id; - private final String attr; - private final Instant time; - private final Set childSet; - - private SimpleImmutableRecordWithSet(Builder b) { - this.id = b.id; - this.attr = b.attr; - this.time = b.time; - this.childSet = b.childSet; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - - public Set getChildSet() { - return Collections.unmodifiableSet(childSet); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private Instant time; - private Set childSet; - - public Builder id(String v) { - this.id = v; - return this; - } - - public Builder attr(String v) { - this.attr = v; - return this; - } - - public Builder time(Instant v) { - this.time = v; - return this; - } - - public Builder childSet(Set v) { - this.childSet = v; - return this; - } - - public SimpleImmutableRecordWithSet build() { - return new SimpleImmutableRecordWithSet(this); - } - } - } - - @DynamoDbImmutable(builder = SimpleImmutableRecordWithMap.Builder.class) - public static class SimpleImmutableRecordWithMap { - private final String id; - private final String attr; - private final Instant time; - private final Map childMap; - - private SimpleImmutableRecordWithMap(Builder b) { - this.id = b.id; - this.attr = b.attr; - this.time = b.time; - this.childMap = b.childMap; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public Map getChildMap() { - return Collections.unmodifiableMap(childMap); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private Instant time; - private Map childMap; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder time(Instant time) { - this.time = time; - return this; - } - - public Builder childMap(Map childMap) { - this.childMap = childMap; - return this; - } - - public SimpleImmutableRecordWithMap build() { - return new SimpleImmutableRecordWithMap(this); - } - } - } - - // simple record used by list/set/map as the deepest nested level - @DynamoDbImmutable(builder = SimpleImmutableChild.Builder.class) - public static final class SimpleImmutableChild { - private final String id; - private final String attr; - private final Instant time; - - private SimpleImmutableChild(Builder b) { - this.id = b.id; - this.attr = b.attr; - this.time = b.time; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private Instant time; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder time(Instant time) { - this.time = time; - return this; - } - - public SimpleImmutableChild build() { - return new SimpleImmutableChild(this); - } - } - } - - - // Nested Immutable Records - @DynamoDbImmutable(builder = NestedImmutableRecordWithList.Builder.class) - public static final class NestedImmutableRecordWithList { - private final String id; - private final String attr; - private final Instant time; - private final NestedImmutableLevel2RecordWithList level2; - private final List level2List; - - private NestedImmutableRecordWithList(Builder b) { - this.id = b.id; - this.attr = b.attr; - this.time = b.time; - this.level2 = b.level2; - this.level2List = b.level2List; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedImmutableLevel2RecordWithList getLevel2() { - return level2; - } - - public List getLevel2List() { - return level2List == null ? null : Collections.unmodifiableList(level2List); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private Instant time; - private NestedImmutableLevel2RecordWithList level2; - private List level2List; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder time(Instant time) { - this.time = time; - return this; - } - - public Builder level2(NestedImmutableLevel2RecordWithList level2) { - this.level2 = level2; - return this; - } - - public Builder level2List(List level2List) { - this.level2List = level2List; - return this; - } - - public NestedImmutableRecordWithList build() { - return new NestedImmutableRecordWithList(this); - } - } - } - - @DynamoDbImmutable(builder = NestedImmutableLevel2RecordWithList.Builder.class) - public static final class NestedImmutableLevel2RecordWithList { - private final String attr; - private final Instant time; - private final NestedImmutableLevel3RecordWithList level3; - private final List level3List; - - private NestedImmutableLevel2RecordWithList(Builder b) { - this.attr = b.attr; - this.time = b.time; - this.level3 = b.level3; - this.level3List = b.level3List; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedImmutableLevel3RecordWithList getLevel3() { - return level3; - } - - public List getLevel3List() { - return level3List; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String attr; - private Instant time; - private NestedImmutableLevel3RecordWithList level3; - private List level3List; - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder time(Instant time) { - this.time = time; - return this; - } - - public Builder level3(NestedImmutableLevel3RecordWithList level3) { - this.level3 = level3; - return this; - } - - public Builder level3List(List level3List) { - this.level3List = level3List; - return this; - } - - public NestedImmutableLevel2RecordWithList build() { - return new NestedImmutableLevel2RecordWithList(this); - } - } - } - - @DynamoDbImmutable(builder = NestedImmutableLevel3RecordWithList.Builder.class) - public static final class NestedImmutableLevel3RecordWithList { - private final String attr; - private final Instant time; - private final NestedImmutableLevel4Record level4; - private final List level4List; - - private NestedImmutableLevel3RecordWithList(Builder b) { - this.attr = b.attr; - this.time = b.time; - this.level4 = b.level4; - this.level4List = b.level4List; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedImmutableLevel4Record getLevel4() { - return level4; - } - - public List getLevel4List() { - return level4List; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String attr; - private Instant time; - private NestedImmutableLevel4Record level4; - private List level4List; - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder time(Instant time) { - this.time = time; - return this; - } - - public Builder level4(NestedImmutableLevel4Record level4) { - this.level4 = level4; - return this; - } - - public Builder level4List(List level4List) { - this.level4List = level4List; - return this; - } - - public NestedImmutableLevel3RecordWithList build() { - return new NestedImmutableLevel3RecordWithList(this); - } - } - } - - @DynamoDbImmutable(builder = NestedImmutableRecordWithSet.Builder.class) - public static final class NestedImmutableRecordWithSet { - private final String id; - private final String attr; - private final Instant time; - private final NestedImmutableLevel2RecordWithSet level2; - private final Set level2Set; - - private NestedImmutableRecordWithSet(Builder b) { - id = b.id; - attr = b.attr; - time = b.time; - level2 = b.level2; - level2Set = b.level2Set; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedImmutableLevel2RecordWithSet getLevel2() { - return level2; - } - - public Set getLevel2Set() { - return level2Set == null ? null : Collections.unmodifiableSet(level2Set); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private Instant time; - private NestedImmutableLevel2RecordWithSet level2; - private Set level2Set; - - public Builder id(String v) { - id = v; - return this; - } - - public Builder attr(String v) { - attr = v; - return this; - } - - public Builder time(Instant v) { - time = v; - return this; - } - - public Builder level2(NestedImmutableLevel2RecordWithSet v) { - level2 = v; - return this; - } - - public Builder level2Set(Set v) { - level2Set = v; - return this; - } - - public NestedImmutableRecordWithSet build() { - return new NestedImmutableRecordWithSet(this); - } - } - } - - @DynamoDbImmutable(builder = NestedImmutableLevel2RecordWithSet.Builder.class) - public static class NestedImmutableLevel2RecordWithSet { - private final String attr; - private final Instant time; - private final NestedImmutableLevel3RecordWithSet level3; - private final Set level3Set; - - private NestedImmutableLevel2RecordWithSet(Builder b) { - attr = b.attr; - time = b.time; - level3 = b.level3; - level3Set = b.level3Set; - } - - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedImmutableLevel3RecordWithSet getLevel3() { - return level3; - } - - public Set getLevel3Set() { - return level3Set == null ? null : Collections.unmodifiableSet(level3Set); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String attr; - private Instant time; - private NestedImmutableLevel3RecordWithSet level3; - private Set level3Set; - - public Builder attr(String v) { - attr = v; - return this; - } - - public Builder time(Instant v) { - time = v; - return this; - } - - public Builder level3(NestedImmutableLevel3RecordWithSet v) { - level3 = v; - return this; - } - - public Builder level3Set(Set v) { - level3Set = v; - return this; - } - - public NestedImmutableLevel2RecordWithSet build() { - return new NestedImmutableLevel2RecordWithSet(this); - } - } - } - - @DynamoDbImmutable(builder = NestedImmutableLevel3RecordWithSet.Builder.class) - public static class NestedImmutableLevel3RecordWithSet { - private final String attr; - private final Instant time; - private final NestedImmutableLevel4Record level4; - private final Set level4Set; - - private NestedImmutableLevel3RecordWithSet(Builder b) { - attr = b.attr; - time = b.time; - level4 = b.level4; - level4Set = b.level4Set; - } - - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedImmutableLevel4Record getLevel4() { - return level4; - } - - public Set getLevel4Set() { - return level4Set == null ? null : Collections.unmodifiableSet(level4Set); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String attr; - private Instant time; - private NestedImmutableLevel4Record level4; - private Set level4Set; - - public Builder attr(String v) { - attr = v; - return this; - } - - public Builder time(Instant v) { - time = v; - return this; - } - - public Builder level4(NestedImmutableLevel4Record v) { - level4 = v; - return this; - } - - public Builder level4Set(Set v) { - level4Set = v; - return this; - } - - public NestedImmutableLevel3RecordWithSet build() { - return new NestedImmutableLevel3RecordWithSet(this); - } - } - } - - @DynamoDbImmutable(builder = NestedImmutableRecordWithMap.Builder.class) - public static class NestedImmutableRecordWithMap { - private final String id; - private final String attr; - private final Instant time; - private final NestedImmutableLevel2RecordWithMap level2; - private final Map level2Map; - - private NestedImmutableRecordWithMap(Builder b) { - this.id = b.id; - this.attr = b.attr; - this.time = b.time; - this.level2 = b.level2; - this.level2Map = b.level2Map; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedImmutableLevel2RecordWithMap getLevel2() { - return level2; - } - - public Map getLevel2Map() { - return level2Map == null ? null : Collections.unmodifiableMap(level2Map); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private Instant time; - private NestedImmutableLevel2RecordWithMap level2; - private Map level2Map; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder time(Instant time) { - this.time = time; - return this; - } - - public Builder level2(NestedImmutableLevel2RecordWithMap v) { - this.level2 = v; - return this; - } - - public Builder level2Map(Map v) { - this.level2Map = v; - return this; - } - - public NestedImmutableRecordWithMap build() { - return new NestedImmutableRecordWithMap(this); - } - } - } - - @DynamoDbImmutable(builder = NestedImmutableLevel2RecordWithMap.Builder.class) - public static class NestedImmutableLevel2RecordWithMap { - private final String attr; - private final Instant time; - private final NestedImmutableLevel3RecordWithMap level3; - private final Map level3Map; - - private NestedImmutableLevel2RecordWithMap(Builder b) { - this.attr = b.attr; - this.time = b.time; - this.level3 = b.level3; - this.level3Map = b.level3Map; - } - - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedImmutableLevel3RecordWithMap getLevel3() { - return level3; - } - - public Map getLevel3Map() { - return level3Map == null ? null : Collections.unmodifiableMap(level3Map); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String attr; - private Instant time; - private NestedImmutableLevel3RecordWithMap level3; - private Map level3Map; - - public Builder attr(String v) { - this.attr = v; - return this; - } - - public Builder time(Instant v) { - this.time = v; - return this; - } - - public Builder level3(NestedImmutableLevel3RecordWithMap v) { - this.level3 = v; - return this; - } - - public Builder level3Map(Map v) { - this.level3Map = v; - return this; - } - - public NestedImmutableLevel2RecordWithMap build() { - return new NestedImmutableLevel2RecordWithMap(this); - } - } - } - - @DynamoDbImmutable(builder = NestedImmutableLevel3RecordWithMap.Builder.class) - public static class NestedImmutableLevel3RecordWithMap { - private final String attr; - private final Instant time; - private final NestedImmutableLevel4Record level4; - private final Map level4Map; - - private NestedImmutableLevel3RecordWithMap(Builder b) { - this.attr = b.attr; - this.time = b.time; - this.level4 = b.level4; - this.level4Map = b.level4Map; - } - - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public NestedImmutableLevel4Record getLevel4() { - return level4; - } - - public Map getLevel4Map() { - return level4Map == null ? null : Collections.unmodifiableMap(level4Map); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String attr; - private Instant time; - private NestedImmutableLevel4Record level4; - private Map level4Map; - - public Builder attr(String v) { - this.attr = v; - return this; - } - - public Builder time(Instant v) { - this.time = v; - return this; - } - - public Builder level4(NestedImmutableLevel4Record v) { - this.level4 = v; - return this; - } - - public Builder level4Map(Map v) { - this.level4Map = v; - return this; - } - - public NestedImmutableLevel3RecordWithMap build() { - return new NestedImmutableLevel3RecordWithMap(this); - } - } - } - - // nested record used by list/set/map as the deepest nested level - @DynamoDbImmutable(builder = NestedImmutableLevel4Record.Builder.class) - public static final class NestedImmutableLevel4Record { - private final String id; - private final String attr; - private final Instant time; - - private NestedImmutableLevel4Record(Builder b) { - this.id = b.id; - this.attr = b.attr; - this.time = b.time; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getTime() { - return time; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private Instant time; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder time(Instant time) { - this.time = time; - return this; - } - - public NestedImmutableLevel4Record build() { - return new NestedImmutableLevel4Record(this); - } - } - } - - - // Simple Static Records - public static class SimpleStaticRecordWithSet { - private String id; - private String attr; - private Instant time; - private Set childSet; - - public String getId() { - return id; - } - - public SimpleStaticRecordWithSet setId(String id) { - this.id = id; - return this; - } - - public String getAttr() { - return attr; - } - - public SimpleStaticRecordWithSet setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public SimpleStaticRecordWithSet setTime(Instant time) { - this.time = time; - return this; - } - - public Set getChildSet() { - return Collections.unmodifiableSet(childSet); - } - - public SimpleStaticRecordWithSet setChildSet(Set childSet) { - this.childSet = childSet; - return this; - } - } - - public static class SimpleStaticRecordWithMap { - private String id; - private String attr; - private Instant time; - private Map childMap; - - public String getId() { - return id; - } - - public SimpleStaticRecordWithMap setId(String id) { - this.id = id; - return this; - } - - public String getAttr() { - return attr; - } - - public SimpleStaticRecordWithMap setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public SimpleStaticRecordWithMap setTime(Instant time) { - this.time = time; - return this; - } - - public Map getChildMap() { - return Collections.unmodifiableMap(childMap); - } - - public SimpleStaticRecordWithMap setChildMap(Map childMap) { - this.childMap = childMap; - return this; - } - } - - public static class SimpleStaticRecordWithList { - private String id; - private String attr; - private Instant time; - private List childList; - - public String getId() { - return id; - } - - public SimpleStaticRecordWithList setId(String id) { - this.id = id; - return this; - } - - - public String getAttr() { - return attr; - } - - public SimpleStaticRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public SimpleStaticRecordWithList setTime(Instant time) { - this.time = time; - return this; - } - - - public List getChildList() { - return childList == null ? null : Collections.unmodifiableList(childList); - } - - public SimpleStaticRecordWithList setChildList(List childList) { - this.childList = childList; - return this; - } - } - - // simple record used by list/set/map as the deepest nested level - public static class SimpleStaticChild { - private String id; - private String attr; - private Instant time; - - public String getId() { - return id; - } - - public SimpleStaticChild setId(String id) { - this.id = id; - return this; - } - - - public String getAttr() { - return attr; - } - - public SimpleStaticChild setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public SimpleStaticChild setTime(Instant time) { - this.time = time; - return this; - } - } - - - // Nested Static Records - public static class NestedStaticRecordWithList { - private String id; - private String attr; - private Instant time; - private NestedStaticLevel2RecordWithList level2; - private List level2List; - - public String getId() { - return id; - } - - public NestedStaticRecordWithList setId(String id) { - this.id = id; - return this; - } - - - public String getAttr() { - return attr; - } - - public NestedStaticRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticRecordWithList setTime(Instant time) { - this.time = time; - return this; - } - - public NestedStaticLevel2RecordWithList getLevel2() { - return level2; - } - - public NestedStaticRecordWithList setLevel2(NestedStaticLevel2RecordWithList level2) { - this.level2 = level2; - return this; - } - - public List getLevel2List() { - return level2List; - } - - public NestedStaticRecordWithList setLevel2List(List level2List) { - this.level2List = level2List; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticRecordWithList)) { - return false; - } - - NestedStaticRecordWithList that = (NestedStaticRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level2, that.level2) && Objects.equals(level2List, that.level2List); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(id); - result = 31 * result + Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level2); - result = 31 * result + Objects.hashCode(level2List); - return result; - } - } - - public static class NestedStaticLevel2RecordWithList { - private String attr; - private Instant time; - private NestedStaticLevel3RecordWithList level3; - private List level3List; - - - public String getAttr() { - return attr; - } - - public NestedStaticLevel2RecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticLevel2RecordWithList setTime(Instant time) { - this.time = time; - return this; - } - - public NestedStaticLevel3RecordWithList getLevel3() { - return level3; - } - - public NestedStaticLevel2RecordWithList setLevel3(NestedStaticLevel3RecordWithList level3) { - this.level3 = level3; - return this; - } - - public List getLevel3List() { - return level3List; - } - - public NestedStaticLevel2RecordWithList setLevel3List(List level3List) { - this.level3List = level3List; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticLevel2RecordWithList)) { - return false; - } - - NestedStaticLevel2RecordWithList that = (NestedStaticLevel2RecordWithList) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level3, that.level3) && Objects.equals(level3List, that.level3List); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level3); - result = 31 * result + Objects.hashCode(level3List); - return result; - } - } - - public static class NestedStaticLevel3RecordWithList { - private String attr; - private Instant time; - private NestedStaticLevel4Record level4; - private List level4List; - - - public String getAttr() { - return attr; - } - - public NestedStaticLevel3RecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticLevel3RecordWithList setTime(Instant time) { - this.time = time; - return this; - } - - public NestedStaticLevel4Record getLevel4() { - return level4; - } - - public NestedStaticLevel3RecordWithList setLevel4(NestedStaticLevel4Record level4) { - this.level4 = level4; - return this; - } - - public List getLevel4List() { - return level4List; - } - - public NestedStaticLevel3RecordWithList setLevel4List(List level4List) { - this.level4List = level4List; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticLevel3RecordWithList)) { - return false; - } - - NestedStaticLevel3RecordWithList that = (NestedStaticLevel3RecordWithList) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level4, that.level4) && Objects.equals(level4List, that.level4List); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level4); - result = 31 * result + Objects.hashCode(level4List); - return result; - } - } - - public static class NestedStaticRecordWithSet { - private String id; - private String attr; - private Instant time; - private NestedStaticLevel2RecordWithSet level2; - private Set level2List; - - public String getId() { - return id; - } - - public NestedStaticRecordWithSet setId(String id) { - this.id = id; - return this; - } - - - public String getAttr() { - return attr; - } - - public NestedStaticRecordWithSet setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticRecordWithSet setTime(Instant time) { - this.time = time; - return this; - } - - public NestedStaticLevel2RecordWithSet getLevel2() { - return level2; - } - - public NestedStaticRecordWithSet setLevel2(NestedStaticLevel2RecordWithSet level2) { - this.level2 = level2; - return this; - } - - public Set getLevel2List() { - return level2List; - } - - public NestedStaticRecordWithSet setLevel2List(Set level2List) { - this.level2List = level2List; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticRecordWithSet)) { - return false; - } - - NestedStaticRecordWithSet that = (NestedStaticRecordWithSet) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level2, that.level2) && Objects.equals(level2List, that.level2List); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(id); - result = 31 * result + Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level2); - result = 31 * result + Objects.hashCode(level2List); - return result; - } - } - - public static class NestedStaticLevel2RecordWithSet { - private String attr; - private Instant time; - private NestedStaticLevel3RecordWithSet level3; - private Set level3Set; - - - public String getAttr() { - return attr; - } - - public NestedStaticLevel2RecordWithSet setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticLevel2RecordWithSet setTime(Instant time) { - this.time = time; - return this; - } - - public NestedStaticLevel3RecordWithSet getLevel3() { - return level3; - } - - public NestedStaticLevel2RecordWithSet setLevel3(NestedStaticLevel3RecordWithSet level3) { - this.level3 = level3; - return this; - } - - public Set getLevel3Set() { - return level3Set; - } - - public NestedStaticLevel2RecordWithSet setLevel3Set(Set level3Set) { - this.level3Set = level3Set; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticLevel2RecordWithSet)) { - return false; - } - - NestedStaticLevel2RecordWithSet that = (NestedStaticLevel2RecordWithSet) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level3, that.level3) && Objects.equals(level3Set, that.level3Set); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level3); - result = 31 * result + Objects.hashCode(level3Set); - return result; - } - } - - public static class NestedStaticLevel3RecordWithSet { - private String attr; - private Instant time; - private NestedStaticLevel4Record level4; - private Set level4Set; - - - public String getAttr() { - return attr; - } - - public NestedStaticLevel3RecordWithSet setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticLevel3RecordWithSet setTime(Instant time) { - this.time = time; - return this; - } - - public NestedStaticLevel4Record getLevel4() { - return level4; - } - - public NestedStaticLevel3RecordWithSet setLevel4(NestedStaticLevel4Record level4) { - this.level4 = level4; - return this; - } - - public Set getLevel4Set() { - return level4Set; - } - - public NestedStaticLevel3RecordWithSet setLevel4Set(Set level4Set) { - this.level4Set = level4Set; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticLevel3RecordWithSet)) { - return false; - } - - NestedStaticLevel3RecordWithSet that = (NestedStaticLevel3RecordWithSet) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level4, that.level4) && Objects.equals(level4Set, that.level4Set); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level4); - result = 31 * result + Objects.hashCode(level4Set); - return result; - } - } - - public static class NestedStaticRecordWithMap { - private String id; - private String attr; - private Instant time; - private NestedStaticLevel2RecordWithMap level2; - private Map level2Map; - - public String getId() { - return id; - } - - public NestedStaticRecordWithMap setId(String id) { - this.id = id; - return this; - } - - - public String getAttr() { - return attr; - } - - public NestedStaticRecordWithMap setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticRecordWithMap setTime(Instant time) { - this.time = time; - return this; - } - - public NestedStaticLevel2RecordWithMap getLevel2() { - return level2; - } - - public NestedStaticRecordWithMap setLevel2(NestedStaticLevel2RecordWithMap level2) { - this.level2 = level2; - return this; - } - - public Map getLevel2Map() { - return level2Map; - } - - public NestedStaticRecordWithMap setLevel2Map(Map level2Map) { - this.level2Map = level2Map; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticRecordWithMap)) { - return false; - } - - NestedStaticRecordWithMap that = (NestedStaticRecordWithMap) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level2, that.level2) && Objects.equals(level2Map, that.level2Map); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(id); - result = 31 * result + Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level2); - result = 31 * result + Objects.hashCode(level2Map); - return result; - } - } - - public static class NestedStaticLevel2RecordWithMap { - private String attr; - private Instant time; - private NestedStaticLevel3RecordWithMap level3; - private Map level3Map; - - - public String getAttr() { - return attr; - } - - public NestedStaticLevel2RecordWithMap setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticLevel2RecordWithMap setTime(Instant time) { - this.time = time; - return this; - } - - public NestedStaticLevel3RecordWithMap getLevel3() { - return level3; - } - - public NestedStaticLevel2RecordWithMap setLevel3(NestedStaticLevel3RecordWithMap level3) { - this.level3 = level3; - return this; - } - - public Map getLevel3Map() { - return level3Map; - } - - public NestedStaticLevel2RecordWithMap setLevel3Map(Map level3Map) { - this.level3Map = level3Map; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticLevel2RecordWithMap)) { - return false; - } - - NestedStaticLevel2RecordWithMap that = (NestedStaticLevel2RecordWithMap) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level3, that.level3) && Objects.equals(level3Map, that.level3Map); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level3); - result = 31 * result + Objects.hashCode(level3Map); - return result; - } - } - - public static class NestedStaticLevel3RecordWithMap { - private String attr; - private Instant time; - private NestedStaticLevel4Record level4; - private Map level4Map; - - public String getAttr() { - return attr; - } - - public NestedStaticLevel3RecordWithMap setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticLevel3RecordWithMap setTime(Instant time) { - this.time = time; - return this; - } - - public NestedStaticLevel4Record getLevel4() { - return level4; - } - - public NestedStaticLevel3RecordWithMap setLevel4(NestedStaticLevel4Record level4) { - this.level4 = level4; - return this; - } - - public Map getLevel4Map() { - return level4Map; - } - - public NestedStaticLevel3RecordWithMap setLevel4Map(Map level4Map) { - this.level4Map = level4Map; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticLevel3RecordWithMap)) { - return false; - } - - NestedStaticLevel3RecordWithMap that = (NestedStaticLevel3RecordWithMap) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time) && Objects.equals(level4, that.level4) && Objects.equals(level4Map, that.level4Map); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - result = 31 * result + Objects.hashCode(level4); - result = 31 * result + Objects.hashCode(level4Map); - return result; - } - } - - // nested record used by list/set/map as the deepest nested level - public static class NestedStaticLevel4Record { - private String attr; - private Instant time; - - public String getAttr() { - return attr; - } - - public NestedStaticLevel4Record setAttr(String attr) { - this.attr = attr; - return this; - } - - public Instant getTime() { - return time; - } - - public NestedStaticLevel4Record setTime(Instant time) { - this.time = time; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof NestedStaticLevel4Record)) { - return false; - } - - NestedStaticLevel4Record that = (NestedStaticLevel4Record) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(time); - return result; - } - } - - - // Bean Table Schemas for Simple Records - public static TableSchema buildBeanSchemaForSimpleRecordWithList() { - return BeanTableSchema.create(SimpleBeanRecordWithList.class); - } - - public static TableSchema buildBeanSchemaForSimpleRecordWithSet() { - return BeanTableSchema.create(SimpleBeanRecordWithSet.class); - } - - public static TableSchema buildBeanSchemaForSimpleRecordWithMap() { - return BeanTableSchema.create(SimpleBeanRecordWithMap.class); - } - - // Bean Table Schemas for Nested Records - public static TableSchema buildBeanSchemaForNestedRecordWithList() { - return BeanTableSchema.create(NestedBeanRecordWithList.class); - } - - public static TableSchema buildBeanSchemaForNestedRecordWithSet() { - return BeanTableSchema.create(NestedBeanRecordWithSet.class); - } - - public static TableSchema buildBeanSchemaForNestedRecordWithMap() { - return BeanTableSchema.create(NestedBeanRecordWithMap.class); - } - - - // Immutable Table Schemas for Simple Records - public static TableSchema buildImmutableSchemaForSimpleRecordWithList() { - return ImmutableTableSchema.create(SimpleImmutableRecordWithList.class); - } - - public static TableSchema buildImmutableSchemaForSimpleRecordWithSet() { - return ImmutableTableSchema.create(SimpleImmutableRecordWithSet.class); - } - - public static TableSchema buildImmutableSchemaForSimpleRecordWithMap() { - return ImmutableTableSchema.create(SimpleImmutableRecordWithMap.class); - } - - - // Immutable Table Schemas for Nested Records - public static TableSchema buildImmutableSchemaForNestedRecordWithList() { - return ImmutableTableSchema.create(NestedImmutableRecordWithList.class); - } - - public static TableSchema buildImmutableSchemaForNestedRecordWithSet() { - return ImmutableTableSchema.create(NestedImmutableRecordWithSet.class); - } - - public static TableSchema buildImmutableSchemaForNestedRecordWithMap() { - return ImmutableTableSchema.create(NestedImmutableRecordWithMap.class); - } - - - // Static Table Schemas for Simple Records - public static TableSchema buildStaticSchemaForSimpleRecordWithList() { - return StaticTableSchema.builder(SimpleStaticRecordWithList.class) - .newItemSupplier(SimpleStaticRecordWithList::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(SimpleStaticRecordWithList::getId) - .setter(SimpleStaticRecordWithList::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(SimpleStaticRecordWithList::getAttr) - .setter(SimpleStaticRecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(SimpleStaticRecordWithList::getTime) - .setter(SimpleStaticRecordWithList::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.listOf(EnhancedType.documentOf( - SimpleStaticChild.class, - buildStaticSchemaForSimpleChildRecord())), - a -> a.name("childList") - .getter(SimpleStaticRecordWithList::getChildList) - .setter(SimpleStaticRecordWithList::setChildList)) - .build(); - } - - public static StaticTableSchema buildStaticSchemaForSimpleRecordWithSet() { - return StaticTableSchema.builder(SimpleStaticRecordWithSet.class) - .newItemSupplier(SimpleStaticRecordWithSet::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(SimpleStaticRecordWithSet::getId) - .setter(SimpleStaticRecordWithSet::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(SimpleStaticRecordWithSet::getAttr) - .setter(SimpleStaticRecordWithSet::setAttr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(SimpleStaticRecordWithSet::getTime) - .setter(SimpleStaticRecordWithSet::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.setOf( - EnhancedType.documentOf( - SimpleStaticChild.class, - buildStaticSchemaForSimpleChildRecord())), - a -> a.name("childSet") - .getter(SimpleStaticRecordWithSet::getChildSet) - .setter(SimpleStaticRecordWithSet::setChildSet)) - .build(); - } - - public static TableSchema buildStaticSchemaForSimpleRecordWithMap() { - return StaticTableSchema.builder(SimpleStaticRecordWithMap.class) - .newItemSupplier(SimpleStaticRecordWithMap::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(SimpleStaticRecordWithMap::getId) - .setter(SimpleStaticRecordWithMap::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(SimpleStaticRecordWithMap::getAttr) - .setter(SimpleStaticRecordWithMap::setAttr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(SimpleStaticRecordWithMap::getTime) - .setter(SimpleStaticRecordWithMap::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.mapOf( - String.class, SimpleStaticChild.class), - a -> a.name("childMap") - .getter(SimpleStaticRecordWithMap::getChildMap) - .setter(SimpleStaticRecordWithMap::setChildMap)) - .build(); - } - - // schema of the simple record used by list/set/map as the deepest simple level - public static TableSchema buildStaticSchemaForSimpleChildRecord() { - return StaticTableSchema.builder(SimpleStaticChild.class) - .newItemSupplier(SimpleStaticChild::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(SimpleStaticChild::getId) - .setter(SimpleStaticChild::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(SimpleStaticChild::getAttr) - .setter(SimpleStaticChild::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(SimpleStaticChild::getTime) - .setter(SimpleStaticChild::setTime) - .tags(autoGeneratedTimestampAttribute())) - .build(); - } - - - // Static Table Schemas for Nested Records - public static TableSchema buildStaticSchemaForNestedRecordWithList() { - return StaticTableSchema.builder(NestedStaticRecordWithList.class) - .newItemSupplier(NestedStaticRecordWithList::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(NestedStaticRecordWithList::getId) - .setter(NestedStaticRecordWithList::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedStaticRecordWithList::getAttr) - .setter(NestedStaticRecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedStaticRecordWithList::getTime) - .setter(NestedStaticRecordWithList::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedStaticLevel2RecordWithList.class, - buildStaticSchemaForNestedLevel2RecordWithList()), - a -> a.name("level2") - .getter(NestedStaticRecordWithList::getLevel2) - .setter(NestedStaticRecordWithList::setLevel2)) - .addAttribute(EnhancedType.listOf(EnhancedType.documentOf( - NestedStaticLevel2RecordWithList.class, - buildStaticSchemaForNestedLevel2RecordWithList())), - a -> a.name("level2List") - .getter(NestedStaticRecordWithList::getLevel2List) - .setter(NestedStaticRecordWithList::setLevel2List)) - .build(); - } - - public static TableSchema buildStaticSchemaForNestedLevel2RecordWithList() { - return StaticTableSchema.builder(NestedStaticLevel2RecordWithList.class) - .newItemSupplier(NestedStaticLevel2RecordWithList::new) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedStaticLevel2RecordWithList::getAttr) - .setter(NestedStaticLevel2RecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedStaticLevel2RecordWithList::getTime) - .setter(NestedStaticLevel2RecordWithList::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedStaticLevel3RecordWithList.class, - buildStaticSchemaForNestedLevel3RecordWithList()), - a -> a.name("level3") - .getter(NestedStaticLevel2RecordWithList::getLevel3) - .setter(NestedStaticLevel2RecordWithList::setLevel3)) - .addAttribute(EnhancedType.listOf(EnhancedType.documentOf( - NestedStaticLevel3RecordWithList.class, - buildStaticSchemaForNestedLevel3RecordWithList())), - a -> a.name("level3List") - .getter(NestedStaticLevel2RecordWithList::getLevel3List) - .setter(NestedStaticLevel2RecordWithList::setLevel3List)) - .build(); - } - - public static TableSchema buildStaticSchemaForNestedLevel3RecordWithList() { - return StaticTableSchema.builder(NestedStaticLevel3RecordWithList.class) - .newItemSupplier(NestedStaticLevel3RecordWithList::new) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedStaticLevel3RecordWithList::getAttr) - .setter(NestedStaticLevel3RecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedStaticLevel3RecordWithList::getTime) - .setter(NestedStaticLevel3RecordWithList::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedStaticLevel4Record.class, - buildStaticSchemaForNestedLevel4Record()), - a -> a.name("level4") - .getter(NestedStaticLevel3RecordWithList::getLevel4) - .setter(NestedStaticLevel3RecordWithList::setLevel4)) - .addAttribute(EnhancedType.listOf( - EnhancedType.documentOf( - NestedStaticLevel4Record.class, - buildStaticSchemaForNestedLevel4Record())), - a -> a.name("level4List") - .getter(NestedStaticLevel3RecordWithList::getLevel4List) - .setter(NestedStaticLevel3RecordWithList::setLevel4List)) - .build(); - } - - public static TableSchema buildStaticSchemaForNestedRecordWithSet() { - return StaticTableSchema.builder(NestedStaticRecordWithSet.class) - .newItemSupplier(NestedStaticRecordWithSet::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(NestedStaticRecordWithSet::getId) - .setter(NestedStaticRecordWithSet::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedStaticRecordWithSet::getAttr) - .setter(NestedStaticRecordWithSet::setAttr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedStaticRecordWithSet::getTime) - .setter(NestedStaticRecordWithSet::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedStaticLevel2RecordWithSet.class, - buildStaticSchemaForNestedLevel2RecordWithSet()), - a -> a.name("level2") - .getter(NestedStaticRecordWithSet::getLevel2) - .setter(NestedStaticRecordWithSet::setLevel2)) - .addAttribute(EnhancedType.setOf(EnhancedType.documentOf( - NestedStaticLevel2RecordWithSet.class, - buildStaticSchemaForNestedLevel2RecordWithSet())), - a -> a.name("level2Set") - .getter(NestedStaticRecordWithSet::getLevel2List) - .setter(NestedStaticRecordWithSet::setLevel2List)) - .build(); - } - - public static TableSchema buildStaticSchemaForNestedLevel2RecordWithSet() { - return StaticTableSchema.builder(NestedStaticLevel2RecordWithSet.class) - .newItemSupplier(NestedStaticLevel2RecordWithSet::new) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedStaticLevel2RecordWithSet::getAttr) - .setter(NestedStaticLevel2RecordWithSet::setAttr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedStaticLevel2RecordWithSet::getTime) - .setter(NestedStaticLevel2RecordWithSet::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedStaticLevel3RecordWithSet.class, - buildStaticSchemaForNestedLevel3RecordWithSet()), - a -> a.name("level3") - .getter(NestedStaticLevel2RecordWithSet::getLevel3) - .setter(NestedStaticLevel2RecordWithSet::setLevel3)) - .addAttribute(EnhancedType.setOf(EnhancedType.documentOf( - NestedStaticLevel3RecordWithSet.class, - buildStaticSchemaForNestedLevel3RecordWithSet())), - a -> a.name("level3Set") - .getter(NestedStaticLevel2RecordWithSet::getLevel3Set) - .setter(NestedStaticLevel2RecordWithSet::setLevel3Set)) - .build(); - } - - public static TableSchema buildStaticSchemaForNestedLevel3RecordWithSet() { - return StaticTableSchema.builder(NestedStaticLevel3RecordWithSet.class) - .newItemSupplier(NestedStaticLevel3RecordWithSet::new) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedStaticLevel3RecordWithSet::getAttr) - .setter(NestedStaticLevel3RecordWithSet::setAttr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedStaticLevel3RecordWithSet::getTime) - .setter(NestedStaticLevel3RecordWithSet::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedStaticLevel4Record.class, - buildStaticSchemaForNestedLevel4Record()), - a -> a.name("level4") - .getter(NestedStaticLevel3RecordWithSet::getLevel4) - .setter(NestedStaticLevel3RecordWithSet::setLevel4)) - .addAttribute(EnhancedType.setOf( - EnhancedType.documentOf( - NestedStaticLevel4Record.class, - buildStaticSchemaForNestedLevel4Record())), - a -> a.name("level4List") - .getter(NestedStaticLevel3RecordWithSet::getLevel4Set) - .setter(NestedStaticLevel3RecordWithSet::setLevel4Set)) - .build(); - } - - public static TableSchema buildStaticSchemaForNestedRecordWithMap() { - return StaticTableSchema.builder(NestedStaticRecordWithMap.class) - .newItemSupplier(NestedStaticRecordWithMap::new) - .addAttribute(String.class, - a -> a.name("id") - .getter(NestedStaticRecordWithMap::getId) - .setter(NestedStaticRecordWithMap::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedStaticRecordWithMap::getAttr) - .setter(NestedStaticRecordWithMap::setAttr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedStaticRecordWithMap::getTime) - .setter(NestedStaticRecordWithMap::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedStaticLevel2RecordWithMap.class, - buildStaticSchemaForNestedLevel2RecordWithMap()), - a -> a.name("level2") - .getter(NestedStaticRecordWithMap::getLevel2) - .setter(NestedStaticRecordWithMap::setLevel2)) - .addAttribute(EnhancedType.mapOf( - String.class, NestedStaticLevel2RecordWithMap.class), - a -> a.name("level2Map") - .getter(NestedStaticRecordWithMap::getLevel2Map) - .setter(NestedStaticRecordWithMap::setLevel2Map)) - .build(); - } - - public static TableSchema buildStaticSchemaForNestedLevel2RecordWithMap() { - return StaticTableSchema.builder(NestedStaticLevel2RecordWithMap.class) - .newItemSupplier(NestedStaticLevel2RecordWithMap::new) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedStaticLevel2RecordWithMap::getAttr) - .setter(NestedStaticLevel2RecordWithMap::setAttr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedStaticLevel2RecordWithMap::getTime) - .setter(NestedStaticLevel2RecordWithMap::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedStaticLevel3RecordWithMap.class, - buildStaticSchemaForNestedLevel3RecordWithMap()), - a -> a.name("level3") - .getter(NestedStaticLevel2RecordWithMap::getLevel3) - .setter(NestedStaticLevel2RecordWithMap::setLevel3)) - .addAttribute(EnhancedType.mapOf( - String.class, NestedStaticLevel3RecordWithMap.class), - a -> a.name("level3Map") - .getter(NestedStaticLevel2RecordWithMap::getLevel3Map) - .setter(NestedStaticLevel2RecordWithMap::setLevel3Map)) - .build(); - } - - public static TableSchema buildStaticSchemaForNestedLevel3RecordWithMap() { - return StaticTableSchema.builder(NestedStaticLevel3RecordWithMap.class) - .newItemSupplier(NestedStaticLevel3RecordWithMap::new) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedStaticLevel3RecordWithMap::getAttr) - .setter(NestedStaticLevel3RecordWithMap::setAttr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedStaticLevel3RecordWithMap::getTime) - .setter(NestedStaticLevel3RecordWithMap::setTime) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedStaticLevel4Record.class, - buildStaticSchemaForNestedLevel4Record()), - a -> a.name("level4") - .getter(NestedStaticLevel3RecordWithMap::getLevel4) - .setter(NestedStaticLevel3RecordWithMap::setLevel4)) - .addAttribute(EnhancedType.mapOf( - String.class, NestedStaticLevel4Record.class), - a -> a.name("level4Map") - .getter(NestedStaticLevel3RecordWithMap::getLevel4Map) - .setter(NestedStaticLevel3RecordWithMap::setLevel4Map)) - .build(); - } - - // schema of the nested record used by list/set/map as the deepest nested level - public static TableSchema buildStaticSchemaForNestedLevel4Record() { - return StaticTableSchema.builder(NestedStaticLevel4Record.class) - .newItemSupplier(NestedStaticLevel4Record::new) - .addAttribute(String.class, - x -> x.name("attr") - .getter(NestedStaticLevel4Record::getAttr) - .setter(NestedStaticLevel4Record::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - x -> x.name("time") - .getter(NestedStaticLevel4Record::getTime) - .setter(NestedStaticLevel4Record::setTime) - .tags(autoGeneratedTimestampAttribute())) - .build(); - } - - - // Static Immutable Table Schemas for Simple Records - public static TableSchema buildStaticImmutableSchemaForSimpleChildRecord() { - return StaticImmutableTableSchema.builder(SimpleImmutableChild.class, SimpleImmutableChild.Builder.class) - .newItemBuilder(SimpleImmutableChild::builder, SimpleImmutableChild.Builder::build) - .addAttribute(String.class, - a -> a.name("id") - .getter(SimpleImmutableChild::getId) - .setter(SimpleImmutableChild.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(SimpleImmutableChild::getAttr) - .setter(SimpleImmutableChild.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(SimpleImmutableChild::getTime) - .setter(SimpleImmutableChild.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForSimpleRecordWithList() { - return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithList.class, - SimpleImmutableRecordWithList.Builder.class) - .newItemBuilder(SimpleImmutableRecordWithList::builder, - SimpleImmutableRecordWithList.Builder::build) - .addAttribute(String.class, - a -> a.name("id") - .getter(SimpleImmutableRecordWithList::getId) - .setter(SimpleImmutableRecordWithList.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(SimpleImmutableRecordWithList::getAttr) - .setter(SimpleImmutableRecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(SimpleImmutableRecordWithList::getTime) - .setter(SimpleImmutableRecordWithList.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.listOf( - EnhancedType.documentOf( - SimpleImmutableChild.class, - buildStaticImmutableSchemaForSimpleChildRecord())), - a -> a.name("childList") - .getter(SimpleImmutableRecordWithList::getChildList) - .setter(SimpleImmutableRecordWithList.Builder::childList)) - .build(); - } - - public static StaticImmutableTableSchema buildStaticImmutableSchemaForSimpleRecordWithSet() { - return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithSet.class, SimpleImmutableRecordWithSet.Builder.class) - .newItemBuilder(SimpleImmutableRecordWithSet::builder, - SimpleImmutableRecordWithSet.Builder::build) - .addAttribute(String.class, - a -> a.name("id") - .getter(SimpleImmutableRecordWithSet::getId) - .setter(SimpleImmutableRecordWithSet.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(SimpleImmutableRecordWithSet::getAttr) - .setter(SimpleImmutableRecordWithSet.Builder::attr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(SimpleImmutableRecordWithSet::getTime) - .setter(SimpleImmutableRecordWithSet.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.setOf( - EnhancedType.documentOf( - SimpleImmutableChild.class, - buildStaticImmutableSchemaForSimpleChildRecord())), - a -> a.name("childSet") - .getter(SimpleImmutableRecordWithSet::getChildSet) - .setter(SimpleImmutableRecordWithSet.Builder::childSet)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForSimpleRecordWithMap() { - return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithMap.class, SimpleImmutableRecordWithMap.Builder.class) - .newItemBuilder(SimpleImmutableRecordWithMap::builder, - SimpleImmutableRecordWithMap.Builder::build) - .addAttribute(String.class, - a -> a.name("id") - .getter(SimpleImmutableRecordWithMap::getId) - .setter(SimpleImmutableRecordWithMap.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(SimpleImmutableRecordWithMap::getAttr) - .setter(SimpleImmutableRecordWithMap.Builder::attr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(SimpleImmutableRecordWithMap::getTime) - .setter(SimpleImmutableRecordWithMap.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.mapOf( - String.class, SimpleImmutableChild.class), - a -> a.name("childMap") - .getter(SimpleImmutableRecordWithMap::getChildMap) - .setter(SimpleImmutableRecordWithMap.Builder::childMap)) - .build(); - } - - - // Static Immutable Table Schemas for Nested Records - public static TableSchema buildStaticImmutableSchemaForNestedRecordWithList() { - return StaticImmutableTableSchema.builder(NestedImmutableRecordWithList.class, - NestedImmutableRecordWithList.Builder.class) - .newItemBuilder(NestedImmutableRecordWithList::builder, - NestedImmutableRecordWithList.Builder::build) - .addAttribute(String.class, - a -> a.name("id") - .getter(NestedImmutableRecordWithList::getId) - .setter(NestedImmutableRecordWithList.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableRecordWithList::getAttr) - .setter(NestedImmutableRecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableRecordWithList::getTime) - .setter(NestedImmutableRecordWithList.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedImmutableLevel2RecordWithList.class, - buildStaticImmutableSchemaForNestedLevel2RecordWithList()), - a -> a.name("level2") - .getter(NestedImmutableRecordWithList::getLevel2) - .setter(NestedImmutableRecordWithList.Builder::level2)) - .addAttribute(EnhancedType.listOf( - EnhancedType.documentOf( - NestedImmutableLevel2RecordWithList.class, - buildStaticImmutableSchemaForNestedLevel2RecordWithList())), - a -> a.name("level2List") - .getter(NestedImmutableRecordWithList::getLevel2List) - .setter(NestedImmutableRecordWithList.Builder::level2List)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForNestedLevel2RecordWithList() { - return StaticImmutableTableSchema.builder(NestedImmutableLevel2RecordWithList.class, - NestedImmutableLevel2RecordWithList.Builder.class) - .newItemBuilder(NestedImmutableLevel2RecordWithList::builder, - NestedImmutableLevel2RecordWithList.Builder::build) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableLevel2RecordWithList::getAttr) - .setter(NestedImmutableLevel2RecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableLevel2RecordWithList::getTime) - .setter(NestedImmutableLevel2RecordWithList.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedImmutableLevel3RecordWithList.class, - buildStaticImmutableSchemaForNestedLevel3RecordWithList()), - a -> a.name("level3") - .getter(NestedImmutableLevel2RecordWithList::getLevel3) - .setter(NestedImmutableLevel2RecordWithList.Builder::level3)) - .addAttribute(EnhancedType.listOf( - EnhancedType.documentOf( - NestedImmutableLevel3RecordWithList.class, - buildStaticImmutableSchemaForNestedLevel3RecordWithList())), - a -> a.name("level3List") - .getter(NestedImmutableLevel2RecordWithList::getLevel3List) - .setter(NestedImmutableLevel2RecordWithList.Builder::level3List)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForNestedLevel3RecordWithList() { - return StaticImmutableTableSchema.builder(NestedImmutableLevel3RecordWithList.class, - NestedImmutableLevel3RecordWithList.Builder.class) - .newItemBuilder(NestedImmutableLevel3RecordWithList::builder, - NestedImmutableLevel3RecordWithList.Builder::build) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableLevel3RecordWithList::getAttr) - .setter(NestedImmutableLevel3RecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableLevel3RecordWithList::getTime) - .setter(NestedImmutableLevel3RecordWithList.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedImmutableLevel4Record.class, - buildStaticImmutableSchemaForNestedLevel4Record()), - a -> a.name("level4") - .getter(NestedImmutableLevel3RecordWithList::getLevel4) - .setter(NestedImmutableLevel3RecordWithList.Builder::level4)) - .addAttribute(EnhancedType.listOf( - EnhancedType.documentOf( - NestedImmutableLevel4Record.class, - buildStaticImmutableSchemaForNestedLevel4Record())), - a -> a.name("level4List") - .getter(NestedImmutableLevel3RecordWithList::getLevel4List) - .setter(NestedImmutableLevel3RecordWithList.Builder::level4List)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForNestedRecordWithSet() { - return StaticImmutableTableSchema.builder(NestedImmutableRecordWithSet.class, - NestedImmutableRecordWithSet.Builder.class) - .newItemBuilder(NestedImmutableRecordWithSet::builder, - NestedImmutableRecordWithSet.Builder::build) - .addAttribute(String.class, - a -> a.name("id") - .getter(NestedImmutableRecordWithSet::getId) - .setter(NestedImmutableRecordWithSet.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableRecordWithSet::getAttr) - .setter(NestedImmutableRecordWithSet.Builder::attr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableRecordWithSet::getTime) - .setter(NestedImmutableRecordWithSet.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedImmutableLevel2RecordWithSet.class, - buildStaticImmutableSchemaForNestedLevel2RecordWithSet()), - a -> a.name("level2") - .getter(NestedImmutableRecordWithSet::getLevel2) - .setter(NestedImmutableRecordWithSet.Builder::level2)) - .addAttribute(EnhancedType.setOf( - EnhancedType.documentOf( - NestedImmutableLevel2RecordWithSet.class, - buildStaticImmutableSchemaForNestedLevel2RecordWithSet())), - a -> a.name("level2Set") - .getter(NestedImmutableRecordWithSet::getLevel2Set) - .setter(NestedImmutableRecordWithSet.Builder::level2Set)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForNestedLevel2RecordWithSet() { - return StaticImmutableTableSchema.builder(NestedImmutableLevel2RecordWithSet.class, - NestedImmutableLevel2RecordWithSet.Builder.class) - .newItemBuilder(NestedImmutableLevel2RecordWithSet::builder, - NestedImmutableLevel2RecordWithSet.Builder::build) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableLevel2RecordWithSet::getAttr) - .setter(NestedImmutableLevel2RecordWithSet.Builder::attr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableLevel2RecordWithSet::getTime) - .setter(NestedImmutableLevel2RecordWithSet.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedImmutableLevel3RecordWithSet.class, - buildStaticImmutableSchemaForNestedLevel3RecordWithSet()), - a -> a.name("level3") - .getter(NestedImmutableLevel2RecordWithSet::getLevel3) - .setter(NestedImmutableLevel2RecordWithSet.Builder::level3)) - .addAttribute(EnhancedType.setOf( - EnhancedType.documentOf( - NestedImmutableLevel3RecordWithSet.class, - buildStaticImmutableSchemaForNestedLevel3RecordWithSet())), - a -> a.name("level3Set") - .getter(NestedImmutableLevel2RecordWithSet::getLevel3Set) - .setter(NestedImmutableLevel2RecordWithSet.Builder::level3Set)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForNestedLevel3RecordWithSet() { - return StaticImmutableTableSchema.builder(NestedImmutableLevel3RecordWithSet.class, - NestedImmutableLevel3RecordWithSet.Builder.class) - .newItemBuilder(NestedImmutableLevel3RecordWithSet::builder, - NestedImmutableLevel3RecordWithSet.Builder::build) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableLevel3RecordWithSet::getAttr) - .setter(NestedImmutableLevel3RecordWithSet.Builder::attr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableLevel3RecordWithSet::getTime) - .setter(NestedImmutableLevel3RecordWithSet.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedImmutableLevel4Record.class, - buildStaticImmutableSchemaForNestedLevel4Record()), - a -> a.name("level4") - .getter(NestedImmutableLevel3RecordWithSet::getLevel4) - .setter(NestedImmutableLevel3RecordWithSet.Builder::level4)) - .addAttribute(EnhancedType.setOf(EnhancedType.documentOf(NestedImmutableLevel4Record.class, - buildStaticImmutableSchemaForNestedLevel4Record())), - a -> a.name("level4Set") - .getter(NestedImmutableLevel3RecordWithSet::getLevel4Set) - .setter(NestedImmutableLevel3RecordWithSet.Builder::level4Set)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForNestedRecordWithMap() { - return StaticImmutableTableSchema.builder(NestedImmutableRecordWithMap.class, - NestedImmutableRecordWithMap.Builder.class) - .newItemBuilder(NestedImmutableRecordWithMap::builder, - NestedImmutableRecordWithMap.Builder::build) - .addAttribute(String.class, - a -> a.name("id") - .getter(NestedImmutableRecordWithMap::getId) - .setter(NestedImmutableRecordWithMap.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableRecordWithMap::getAttr) - .setter(NestedImmutableRecordWithMap.Builder::attr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableRecordWithMap::getTime) - .setter(NestedImmutableRecordWithMap.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedImmutableLevel2RecordWithMap.class, - buildStaticImmutableSchemaForNestedLevel2RecordWithMap()), - a -> a.name("level2") - .getter(NestedImmutableRecordWithMap::getLevel2) - .setter(NestedImmutableRecordWithMap.Builder::level2)) - .addAttribute(EnhancedType.mapOf( - String.class, NestedImmutableLevel2RecordWithMap.class), - a -> a.name("childMap") - .getter(NestedImmutableRecordWithMap::getLevel2Map) - .setter(NestedImmutableRecordWithMap.Builder::level2Map)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForNestedLevel2RecordWithMap() { - return StaticImmutableTableSchema.builder(NestedImmutableLevel2RecordWithMap.class, - NestedImmutableLevel2RecordWithMap.Builder.class) - .newItemBuilder(NestedImmutableLevel2RecordWithMap::builder, - NestedImmutableLevel2RecordWithMap.Builder::build) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableLevel2RecordWithMap::getAttr) - .setter(NestedImmutableLevel2RecordWithMap.Builder::attr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableLevel2RecordWithMap::getTime) - .setter(NestedImmutableLevel2RecordWithMap.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedImmutableLevel3RecordWithMap.class, - buildStaticImmutableSchemaForNestedLevel3RecordWithMap()), - a -> a.name("level3") - .getter(NestedImmutableLevel2RecordWithMap::getLevel3) - .setter(NestedImmutableLevel2RecordWithMap.Builder::level3)) - .addAttribute(EnhancedType.mapOf( - String.class, NestedImmutableLevel3RecordWithMap.class), - a -> a.name("childMap") - .getter(NestedImmutableLevel2RecordWithMap::getLevel3Map) - .setter(NestedImmutableLevel2RecordWithMap.Builder::level3Map)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForNestedLevel3RecordWithMap() { - return StaticImmutableTableSchema.builder(NestedImmutableLevel3RecordWithMap.class, - NestedImmutableLevel3RecordWithMap.Builder.class) - .newItemBuilder(NestedImmutableLevel3RecordWithMap::builder, - NestedImmutableLevel3RecordWithMap.Builder::build) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableLevel3RecordWithMap::getAttr) - .setter(NestedImmutableLevel3RecordWithMap.Builder::attr)) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableLevel3RecordWithMap::getTime) - .setter(NestedImmutableLevel3RecordWithMap.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(EnhancedType.documentOf( - NestedImmutableLevel4Record.class, - buildStaticImmutableSchemaForNestedLevel4Record()), - a -> a.name("level4") - .getter(NestedImmutableLevel3RecordWithMap::getLevel4) - .setter(NestedImmutableLevel3RecordWithMap.Builder::level4)) - .addAttribute(EnhancedType.mapOf( - String.class, NestedImmutableLevel4Record.class), - a -> a.name("level4Map") - .getter(NestedImmutableLevel3RecordWithMap::getLevel4Map) - .setter(NestedImmutableLevel3RecordWithMap.Builder::level4Map)) - .build(); - } - - // schema of the record used by list/set/map as the deepest nested level - public static TableSchema buildStaticImmutableSchemaForNestedLevel4Record() { - return StaticImmutableTableSchema.builder(NestedImmutableLevel4Record.class, NestedImmutableLevel4Record.Builder.class) - .newItemBuilder(NestedImmutableLevel4Record::builder, - NestedImmutableLevel4Record.Builder::build) - .addAttribute(String.class, - a -> a.name("attr") - .getter(NestedImmutableLevel4Record::getAttr) - .setter(NestedImmutableLevel4Record.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(Instant.class, - a -> a.name("time") - .getter(NestedImmutableLevel4Record::getTime) - .setter(NestedImmutableLevel4Record.Builder::time) - .tags(autoGeneratedTimestampAttribute())) - .build(); - } - - - // Object builder methods - public static SimpleBeanRecordWithList buildSimpleBeanRecordWithList() { - return new SimpleBeanRecordWithList() - .setId(ID_1).setAttr(ATTR_LEVEL1) - .setChildList(new ArrayList<>(Arrays.asList( - new SimpleBeanChild().setId(ID_1).setAttr(ATTR_CHILD1), - new SimpleBeanChild().setId(ID_2).setAttr(ATTR_CHILD2)))); - } - - public static SimpleStaticRecordWithList buildSimpleStaticRecordWithList() { - return new SimpleStaticRecordWithList() - .setId(ID_1).setAttr(ATTR_LEVEL1) - .setChildList(new ArrayList<>(Arrays.asList( - new SimpleStaticChild().setId(ID_1).setAttr(ATTR_CHILD1), - new SimpleStaticChild().setId(ID_2).setAttr(ATTR_CHILD2)))); - } - - public static SimpleImmutableRecordWithList buildSimpleImmutableRecordWithList() { - return SimpleImmutableRecordWithList.builder() - .id(ID_1).attr(ATTR_LEVEL1) - .childList(new ArrayList<>(Arrays.asList( - SimpleImmutableChild.builder().id(ID_1).attr(ATTR_CHILD1).build(), - SimpleImmutableChild.builder().id(ID_2).attr(ATTR_CHILD2).build()))) - .build(); - } - - public static NestedBeanRecordWithList buildNestedBeanRecordWithList() { - NestedBeanLevel4Record level4 = - new NestedBeanLevel4Record() - .setId(ID_1) - .setAttr(ATTR_LEVEL4); - - NestedBeanLevel3RecordWithList level3 = - new NestedBeanLevel3RecordWithList() - .setAttr(ATTR_LEVEL3) - .setLevel4(level4) - .setLevel4List(new ArrayList<>(singletonList(level4))); - - NestedBeanLevel2RecordWithList level2 = - new NestedBeanLevel2RecordWithList() - .setAttr(ATTR_LEVEL2).setLevel3(level3) - .setLevel3List(new ArrayList<>(singletonList(level3))); - - return new NestedBeanRecordWithList() - .setId(ID_1).setAttr(ATTR_LEVEL1) - .setLevel2(level2) - .setLevel2List(new ArrayList<>(singletonList(level2))); - } - - public static NestedStaticRecordWithList buildNestedStaticRecordWithList() { - NestedStaticLevel4Record level4 = - new NestedStaticLevel4Record() - .setAttr(ATTR_LEVEL4); - - NestedStaticLevel3RecordWithList level3 = - new NestedStaticLevel3RecordWithList() - .setAttr(ATTR_LEVEL3) - .setLevel4(level4) - .setLevel4List(new ArrayList<>(singletonList(level4))); - - NestedStaticLevel2RecordWithList level2 = - new NestedStaticLevel2RecordWithList() - .setAttr(ATTR_LEVEL2) - .setLevel3(level3) - .setLevel3List(new ArrayList<>(singletonList(level3))); - - return new NestedStaticRecordWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) - .setLevel2(level2) - .setLevel2List(new ArrayList<>(singletonList(level2))); - } - - public static NestedImmutableRecordWithList buildNestedImmutableRecordWithList() { - NestedImmutableLevel4Record level4 = - NestedImmutableLevel4Record.builder() - .id(ID_ATTR) - .attr(ATTR_LEVEL4) - .build(); - - NestedImmutableLevel3RecordWithList level3 = - NestedImmutableLevel3RecordWithList.builder() - .attr(ATTR_LEVEL3) - .level4(level4) - .level4List(new ArrayList<>(singletonList(level4))) - .build(); - - NestedImmutableLevel2RecordWithList level2 = - NestedImmutableLevel2RecordWithList.builder() - .attr(ATTR_LEVEL2) - .level3(level3) - .level3List(new ArrayList<>(singletonList(level3))) - .build(); - - return NestedImmutableRecordWithList.builder() - .id(ID_1) - .attr(ATTR_LEVEL1).level2(level2) - .level2List(new ArrayList<>(singletonList(level2))) - .build(); - } - - public static SimpleBeanRecordWithMap buildSimpleBeanRecordWithMap() { - return new SimpleBeanRecordWithMap() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) - .setChildMap(ImmutableMap.of( - CHILD1_KEY, new SimpleBeanChild().setId(ID_1).setAttr(ATTR_CHILD1), - CHILD2_KEY, new SimpleBeanChild().setId(ID_2).setAttr(ATTR_CHILD2) - )); - } - - public static NestedBeanRecordWithMap buildNestedBeanRecordWithMap() { - NestedBeanLevel4Record level4 = - new NestedBeanLevel4Record() - .setId(ID_1) - .setAttr(ATTR_LEVEL4); - - NestedBeanLevel3RecordWithMap level3 = - new NestedBeanLevel3RecordWithMap() - .setAttr(ATTR_LEVEL3) - .setLevel4(level4) - .setLevel4Map(ImmutableMap.of(LEVEL4_KEY, level4)); - - NestedBeanLevel2RecordWithMap level2 = - new NestedBeanLevel2RecordWithMap() - .setAttr(ATTR_LEVEL2) - .setLevel3(level3) - .setLevel3Map(ImmutableMap.of(LEVEL3_KEY, level3)); - - return new NestedBeanRecordWithMap() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) - .setLevel2(level2) - .setLevel2Map(ImmutableMap.of(LEVEL2_KEY, level2)); - } - - public static SimpleImmutableRecordWithMap buildSimpleImmutableRecordWithMap() { - return SimpleImmutableRecordWithMap.builder() - .id(ID_1).attr(ATTR_LEVEL1) - .childMap(ImmutableMap.of( - CHILD1_KEY, SimpleImmutableChild.builder().id(ID_1).attr(ATTR_CHILD1).build(), - CHILD2_KEY, SimpleImmutableChild.builder().id(ID_2).attr(ATTR_CHILD2).build())) - .build(); - } - - public static NestedImmutableRecordWithMap buildNestedImmutableRecordWithMap() { - NestedImmutableLevel4Record level4 = - NestedImmutableLevel4Record.builder() - .id(ID_1) - .attr(ATTR_LEVEL4) - .build(); - - NestedImmutableLevel3RecordWithMap level3 = - NestedImmutableLevel3RecordWithMap.builder() - .attr(ATTR_LEVEL3) - .level4(level4) - .level4Map(new HashMap<>(singletonMap(LEVEL4_KEY, level4))) - .build(); - - NestedImmutableLevel2RecordWithMap level2 = - NestedImmutableLevel2RecordWithMap.builder() - .attr(ATTR_LEVEL2) - .level3(level3) - .level3Map(new HashMap<>(singletonMap(LEVEL3_KEY, level3))) - .build(); - - return NestedImmutableRecordWithMap.builder() - .id(ID_1) - .attr(ATTR_LEVEL1) - .level2(level2) - .level2Map(new HashMap<>(singletonMap(LEVEL2_KEY, level2))) - .build(); - } -} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedStructureTestModels.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedStructureTestModels.java new file mode 100644 index 000000000000..b76248b0061f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedStructureTestModels.java @@ -0,0 +1,1105 @@ +/* + * 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 static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; +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.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; + +/** + * Shared models used by various tests for nested structures, auto-generated timestamps, and update behavior testing. This file + * contains models for different schema types (Bean, Immutable, Static, Static-Immutable) with various nesting levels and + * collection types. + */ +public final class NestedStructureTestModels { + + private NestedStructureTestModels() { + } + + public static final String ID_1 = "1"; + public static final String ATTR_LEVEL1 = "attr_level1"; + + @DynamoDbBean + public static class SimpleBeanWithList { + private String id; + private String attr; + private Instant time; + private List childList; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanWithList setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public SimpleBeanWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public SimpleBeanWithList setTime(Instant time) { + this.time = time; + return this; + } + + public List getChildList() { + return childList == null ? null : Collections.unmodifiableList(childList); + } + + public SimpleBeanWithList setChildList(List childList) { + this.childList = childList; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleBeanWithList that = (SimpleBeanWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + Objects.equals(time, that.time) && Objects.equals(childList, that.childList); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, time, childList); + } + } + + @DynamoDbBean + public static class SimpleBeanWithSet { + private String id; + private String attr; + private Instant time; + private Set childSet; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanWithSet setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public SimpleBeanWithSet setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public SimpleBeanWithSet setTime(Instant time) { + this.time = time; + return this; + } + + public Set getChildSet() { + return childSet == null ? null : Collections.unmodifiableSet(childSet); + } + + public SimpleBeanWithSet setChildSet(Set childSet) { + this.childSet = childSet; + return this; + } + } + + @DynamoDbBean + public static class SimpleBeanWithMap { + private String id; + private String attr; + private Instant time; + private Map childMap; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanWithMap setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public SimpleBeanWithMap setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public SimpleBeanWithMap setTime(Instant time) { + this.time = time; + return this; + } + + public Map getChildMap() { + return childMap == null ? null : Collections.unmodifiableMap(childMap); + } + + public SimpleBeanWithMap setChildMap(Map childMap) { + this.childMap = childMap; + return this; + } + } + + @DynamoDbBean + public static class SimpleBeanChild { + private String id; + private String attr; + private Instant time; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanChild setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public SimpleBeanChild setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public SimpleBeanChild setTime(Instant time) { + this.time = time; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleBeanChild that = (SimpleBeanChild) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, time); + } + } + + @DynamoDbBean + public static class NestedBeanWithList { + private String id; + private String attr; + private Instant time; + private NestedBeanChild level2; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public NestedBeanWithList setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public NestedBeanWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanWithList setTime(Instant time) { + this.time = time; + return this; + } + + public NestedBeanChild getLevel2() { + return level2; + } + + public NestedBeanWithList setLevel2(NestedBeanChild level2) { + this.level2 = level2; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedBeanWithList that = (NestedBeanWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + Objects.equals(time, that.time) && Objects.equals(level2, that.level2); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, time, level2); + } + } + + @DynamoDbBean + public static class NestedBeanChild { + private String attr; + private Instant time; + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public NestedBeanChild setAttr(String attr) { + this.attr = attr; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedBeanChild setTime(Instant time) { + this.time = time; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedBeanChild that = (NestedBeanChild) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(attr, time); + } + } + + @DynamoDbImmutable(builder = SimpleImmutableRecordWithList.Builder.class) + public static final class SimpleImmutableRecordWithList { + private final String id; + private final String attr; + private final Instant time; + private final List childList; + + private SimpleImmutableRecordWithList(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + this.childList = b.childList; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public List getChildList() { + return childList == null ? null : Collections.unmodifiableList(childList); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private List childList; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder childList(List childList) { + this.childList = childList; + return this; + } + + public SimpleImmutableRecordWithList build() { + return new SimpleImmutableRecordWithList(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleImmutableRecordWithList that = (SimpleImmutableRecordWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + Objects.equals(time, that.time) && Objects.equals(childList, that.childList); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, time, childList); + } + } + + @DynamoDbImmutable(builder = SimpleImmutableChild.Builder.class) + public static final class SimpleImmutableChild { + private final String id; + private final String attr; + private final Instant time; + + private SimpleImmutableChild(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public SimpleImmutableChild build() { + return new SimpleImmutableChild(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleImmutableChild that = (SimpleImmutableChild) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, time); + } + } + + @DynamoDbImmutable(builder = SimpleImmutableRecordWithSet.Builder.class) + public static final class SimpleImmutableRecordWithSet { + private final String id; + private final String attr; + private final Instant time; + private final Set childSet; + + private SimpleImmutableRecordWithSet(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + this.childSet = b.childSet; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public Set getChildSet() { + return childSet == null ? null : Collections.unmodifiableSet(childSet); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private Set childSet; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder childSet(Set childSet) { + this.childSet = childSet; + return this; + } + + public SimpleImmutableRecordWithSet build() { + return new SimpleImmutableRecordWithSet(this); + } + } + } + + @DynamoDbImmutable(builder = SimpleImmutableRecordWithMap.Builder.class) + public static final class SimpleImmutableRecordWithMap { + private final String id; + private final String attr; + private final Instant time; + private final Map childMap; + + private SimpleImmutableRecordWithMap(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + this.childMap = b.childMap; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public Map getChildMap() { + return childMap == null ? null : Collections.unmodifiableMap(childMap); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private Map childMap; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder childMap(Map childMap) { + this.childMap = childMap; + return this; + } + + public SimpleImmutableRecordWithMap build() { + return new SimpleImmutableRecordWithMap(this); + } + } + } + + @DynamoDbImmutable(builder = NestedImmutableRecordWithList.Builder.class) + public static final class NestedImmutableRecordWithList { + private final String id; + private final String attr; + private final Instant time; + private final NestedImmutableChildRecordWithList level2; + + private NestedImmutableRecordWithList(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.time = b.time; + this.level2 = b.level2; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public NestedImmutableChildRecordWithList getLevel2() { + return level2; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private Instant time; + private NestedImmutableChildRecordWithList level2; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public Builder level2(NestedImmutableChildRecordWithList level2) { + this.level2 = level2; + return this; + } + + public NestedImmutableRecordWithList build() { + return new NestedImmutableRecordWithList(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedImmutableRecordWithList that = (NestedImmutableRecordWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + Objects.equals(time, that.time) && Objects.equals(level2, that.level2); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, time, level2); + } + } + + @DynamoDbImmutable(builder = NestedImmutableChildRecordWithList.Builder.class) + public static final class NestedImmutableChildRecordWithList { + private final String attr; + private final Instant time; + + private NestedImmutableChildRecordWithList(Builder b) { + this.attr = b.attr; + this.time = b.time; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getTime() { + return time; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String attr; + private Instant time; + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder time(Instant time) { + this.time = time; + return this; + } + + public NestedImmutableChildRecordWithList build() { + return new NestedImmutableChildRecordWithList(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedImmutableChildRecordWithList that = (NestedImmutableChildRecordWithList) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(attr, time); + } + } + + public static class SimpleStaticRecordWithList { + private String id; + private String attr; + private Instant time; + + public String getId() { + return id; + } + + public SimpleStaticRecordWithList setId(String id) { + this.id = id; + return this; + } + + public String getAttr() { + return attr; + } + + public SimpleStaticRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public SimpleStaticRecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleStaticRecordWithList that = (SimpleStaticRecordWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, time); + } + } + + public static class NestedStaticRecordWithList { + private String id; + private String attr; + private Instant time; + private NestedStaticChildRecordWithList level2; + + public String getId() { + return id; + } + + public NestedStaticRecordWithList setId(String id) { + this.id = id; + return this; + } + + public String getAttr() { + return attr; + } + + public NestedStaticRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticRecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + public NestedStaticChildRecordWithList getLevel2() { + return level2; + } + + public NestedStaticRecordWithList setLevel2(NestedStaticChildRecordWithList level2) { + this.level2 = level2; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedStaticRecordWithList that = (NestedStaticRecordWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + Objects.equals(time, that.time) && Objects.equals(level2, that.level2); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, time, level2); + } + } + + public static class NestedStaticChildRecordWithList { + private String attr; + private Instant time; + + public String getAttr() { + return attr; + } + + public NestedStaticChildRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public Instant getTime() { + return time; + } + + public NestedStaticChildRecordWithList setTime(Instant time) { + this.time = time; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedStaticChildRecordWithList that = (NestedStaticChildRecordWithList) o; + return Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(attr, time); + } + } + + public static TableSchema buildStaticSchemaForSimpleRecordWithList() { + return StaticTableSchema.builder(SimpleStaticRecordWithList.class) + .newItemSupplier(SimpleStaticRecordWithList::new) + .addAttribute(String.class, a -> a.name("id") + .getter(SimpleStaticRecordWithList::getId) + .setter(SimpleStaticRecordWithList::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(SimpleStaticRecordWithList::getAttr) + .setter(SimpleStaticRecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, a -> a.name("time") + .getter(SimpleStaticRecordWithList::getTime) + .setter(SimpleStaticRecordWithList::setTime) + .tags(autoGeneratedTimestampAttribute())) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedRecordWithList() { + TableSchema level2Schema = + StaticTableSchema.builder(NestedStaticChildRecordWithList.class) + .newItemSupplier(NestedStaticChildRecordWithList::new) + .addAttribute(String.class, a -> a.name("attr") + .getter(NestedStaticChildRecordWithList::getAttr) + .setter(NestedStaticChildRecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, a -> a.name("time") + .getter(NestedStaticChildRecordWithList::getTime) + .setter(NestedStaticChildRecordWithList::setTime) + .tags(autoGeneratedTimestampAttribute())) + .build(); + + return StaticTableSchema.builder(NestedStaticRecordWithList.class) + .newItemSupplier(NestedStaticRecordWithList::new) + .addAttribute(String.class, a -> a.name("id") + .getter(NestedStaticRecordWithList::getId) + .setter(NestedStaticRecordWithList::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(NestedStaticRecordWithList::getAttr) + .setter(NestedStaticRecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, a -> a.name("time") + .getter(NestedStaticRecordWithList::getTime) + .setter(NestedStaticRecordWithList::setTime) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf(NestedStaticChildRecordWithList.class, level2Schema), + a -> a.name("level2") + .getter(NestedStaticRecordWithList::getLevel2) + .setter(NestedStaticRecordWithList::setLevel2)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForSimpleRecordWithList() { + TableSchema childSchema = + StaticImmutableTableSchema.builder(SimpleImmutableChild.class, SimpleImmutableChild.Builder.class) + .newItemBuilder(SimpleImmutableChild::builder, + SimpleImmutableChild.Builder::build) + .addAttribute(String.class, a -> a.name("id") + .getter(SimpleImmutableChild::getId) + .setter(SimpleImmutableChild.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(SimpleImmutableChild::getAttr) + .setter(SimpleImmutableChild.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, a -> a.name("time") + .getter(SimpleImmutableChild::getTime) + .setter(SimpleImmutableChild.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .build(); + + return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithList.class, SimpleImmutableRecordWithList.Builder.class) + .newItemBuilder(SimpleImmutableRecordWithList::builder, + SimpleImmutableRecordWithList.Builder::build) + .addAttribute(String.class, a -> a.name("id") + .getter(SimpleImmutableRecordWithList::getId) + .setter(SimpleImmutableRecordWithList.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(SimpleImmutableRecordWithList::getAttr) + .setter(SimpleImmutableRecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, a -> a.name("time") + .getter(SimpleImmutableRecordWithList::getTime) + .setter(SimpleImmutableRecordWithList.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.listOf(EnhancedType.documentOf(SimpleImmutableChild.class, + childSchema)), + a -> a.name("childList") + .getter(SimpleImmutableRecordWithList::getChildList) + .setter(SimpleImmutableRecordWithList.Builder::childList)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedRecordWithList() { + TableSchema level2Schema = + StaticImmutableTableSchema.builder(NestedImmutableChildRecordWithList.class, + NestedImmutableChildRecordWithList.Builder.class) + .newItemBuilder(NestedImmutableChildRecordWithList::builder, + NestedImmutableChildRecordWithList.Builder::build) + .addAttribute(String.class, a -> a.name("attr") + .getter(NestedImmutableChildRecordWithList::getAttr) + .setter(NestedImmutableChildRecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, a -> a.name("time") + .getter(NestedImmutableChildRecordWithList::getTime) + .setter(NestedImmutableChildRecordWithList.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .build(); + + return StaticImmutableTableSchema.builder(NestedImmutableRecordWithList.class, + NestedImmutableRecordWithList.Builder.class) + .newItemBuilder(NestedImmutableRecordWithList::builder, + NestedImmutableRecordWithList.Builder::build) + .addAttribute(String.class, a -> a.name("id") + .getter(NestedImmutableRecordWithList::getId) + .setter(NestedImmutableRecordWithList.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(NestedImmutableRecordWithList::getAttr) + .setter(NestedImmutableRecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(Instant.class, a -> a.name("time") + .getter(NestedImmutableRecordWithList::getTime) + .setter(NestedImmutableRecordWithList.Builder::time) + .tags(autoGeneratedTimestampAttribute())) + .addAttribute(EnhancedType.documentOf(NestedImmutableChildRecordWithList.class, + level2Schema), + a -> a.name("level2") + .getter(NestedImmutableRecordWithList::getLevel2) + .setter(NestedImmutableRecordWithList.Builder::level2)) + .build(); + } +} \ No newline at end of file From 63b6c82143b69ddf35cc3f98ceb750110cdd36e1 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 4 Feb 2026 05:26:23 +0200 Subject: [PATCH 08/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- ... AutoGeneratedTimestampExtensionTest.java} | 255 +++---- .../NestedUpdateBehaviorTest.java | 237 +++--- ... => AutogeneratedTimestampTestModels.java} | 267 +------ .../models/UpdateBehaviorTestModels.java | 715 ++++++++++++++++++ 4 files changed, 950 insertions(+), 524 deletions(-) rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/{NestedAutoGeneratedTimestampTest.java => AutoGeneratedTimestampExtensionTest.java} (86%) rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/{NestedStructureTestModels.java => AutogeneratedTimestampTestModels.java} (75%) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UpdateBehaviorTestModels.java diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedAutoGeneratedTimestampTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java similarity index 86% rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedAutoGeneratedTimestampTest.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java index afc3f57afc0f..1b5bc3f38f5a 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedAutoGeneratedTimestampTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java @@ -21,12 +21,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.ATTR_LEVEL1; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.ID_1; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticImmutableSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticImmutableSchemaForSimpleRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.buildStaticImmutableSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.buildStaticImmutableSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.buildStaticSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.buildStaticSchemaForSimpleRecordWithList; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; @@ -53,18 +51,18 @@ 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.functionaltests.models.NestedStructureTestModels; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedBeanChild; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedBeanWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedStaticRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleBeanWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleBeanWithMap; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleBeanWithSet; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleImmutableRecordWithMap; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleImmutableRecordWithSet; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedBeanChild; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedBeanWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleBeanWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleBeanWithMap; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleBeanWithSet; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithMap; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithSet; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleStaticRecordWithList; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnRoot; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; @@ -77,7 +75,7 @@ import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; -public class NestedAutoGeneratedTimestampTest extends LocalDynamoDbSyncTestBase { +public class AutoGeneratedTimestampExtensionTest extends LocalDynamoDbSyncTestBase { private static final Instant MOCKED_INSTANT_NOW = Instant.now(Clock.fixed(Instant.parse("2019-01-13T14:00:00Z"), ZoneOffset.UTC)); @@ -345,7 +343,7 @@ public void putNewRecord_setsTimestampsOnAlNestedLevels() { DynamoDbTable table = enhancedClient.table(tableName, createNestedRecordSchema()); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - NestedBeanChild nestedLevel1 = new NestedBeanChild().setAttr("attrL1"); + NestedBeanChild nestedLevel1 = new NestedBeanChild(); NestedRecord item = new NestedRecord() .setId("id") @@ -374,24 +372,22 @@ public void updateNestedRecord_updatesTimestampsOnAllLevels() { // Initial put table.putItem(r -> r.item(new NestedRecord().setId("id").setAttribute("one") - .setNestedRecord(new NestedBeanChild().setAttr("attribute")))); + .setNestedRecord(new NestedBeanChild()))); // First update Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); table.updateItem(r -> r.item(new NestedRecord().setId("id").setAttribute("one") - .setNestedRecord(new NestedBeanChild().setAttr("attribute1")))); + .setNestedRecord(new NestedBeanChild()))); GetItemResponse stored = getItemFromDDB(table.tableName(), "id"); - assertThat(stored.item().get("nestedRecord").m().get("attr").s(), is("attribute1")); assertThat(stored.item().get("nestedRecord").m().get("time").s(), is(MOCKED_INSTANT_UPDATE_ONE.toString())); // Second update Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_TWO); table.updateItem(r -> r.item(new NestedRecord().setId("id").setAttribute("one") - .setNestedRecord(new NestedBeanChild().setAttr("attribute2")))); + .setNestedRecord(new NestedBeanChild()))); stored = getItemFromDDB(table.tableName(), "id"); - assertThat(stored.item().get("nestedRecord").m().get("attr").s(), is("attribute2")); assertThat(stored.item().get("nestedRecord").m().get("time").s(), is(MOCKED_INSTANT_UPDATE_TWO.toString())); table.deleteTable(); @@ -423,13 +419,13 @@ public void recursiveRecord_allTimestampsAreUpdated() { // Assert l2 timestamp is set Map childMap = item.get("child").m(); - assertNotNull(childMap.get("childTimestamp1")); - assertEquals(MOCKED_INSTANT_NOW.toString(), childMap.get("childTimestamp1").s()); + assertNotNull(childMap.get("childTimestamp")); + assertEquals(MOCKED_INSTANT_NOW.toString(), childMap.get("childTimestamp").s()); // Assert l3 timestamp is set Map grandchildMap = childMap.get("child").m(); - assertNotNull(grandchildMap.get("grandchildTimestamp1")); - assertEquals(MOCKED_INSTANT_NOW.toString(), grandchildMap.get("grandchildTimestamp1").s()); + assertNotNull(grandchildMap.get("grandchildTimestamp")); + assertEquals(MOCKED_INSTANT_NOW.toString(), grandchildMap.get("grandchildTimestamp").s()); table.deleteTable(); } @@ -443,19 +439,15 @@ public void beanSchema_simpleRecordWithList_populatesTimestamps() { table.putItem( new SimpleBeanWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) + .setId("1") .setChildList(Arrays.asList( - new NestedStructureTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), - new NestedStructureTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2")))); + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1"), + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2")))); SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr(), is("attr_level1")); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(0).getAttr(), is("attr_child1")); assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(1).getAttr(), is("attr_child2")); assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); table.deleteTable(); @@ -469,14 +461,12 @@ public void beanSchema_simpleRecordWithSet_populatesTimestamps() { table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( - new SimpleBeanWithSet() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) + new AutogeneratedTimestampTestModels.SimpleBeanWithSet() + .setId("1") .setChildSet(new HashSet<>(Arrays.asList("child1", "child2")))); - SimpleBeanWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + AutogeneratedTimestampTestModels.SimpleBeanWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr(), is("attr_level1")); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); assertThat(result.getChildSet(), hasSize(2)); assertThat(result.getChildSet().contains("child1"), is(true)); @@ -488,13 +478,12 @@ public void beanSchema_simpleRecordWithSet_populatesTimestamps() { @Test public void beanSchema_simpleRecordWithMap_populatesTimestamps() { String tableName = getConcreteTableName("bean-simple-map-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(SimpleBeanWithMap.class)); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(AutogeneratedTimestampTestModels.SimpleBeanWithMap.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( - new SimpleBeanWithMap() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) + new AutogeneratedTimestampTestModels.SimpleBeanWithMap() + .setId("1") .setChildMap(new HashMap() {{ put("child1", "attr_child1"); put("child2", "attr_child2"); @@ -502,7 +491,6 @@ public void beanSchema_simpleRecordWithMap_populatesTimestamps() { SimpleBeanWithMap result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr(), is("attr_level1")); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); assertThat(result.getChildMap().size(), is(2)); assertThat(result.getChildMap().get("child1"), is("attr_child1")); @@ -519,15 +507,12 @@ public void beanSchema_nestedRecordWithList_populatesTimestamps() { table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( new NestedBeanWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) - .setLevel2(new NestedBeanChild().setAttr("attr_level2"))); + .setId("1") + .setLevel2(new NestedBeanChild())); NestedBeanWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(level1.getAttr(), is("attr_level1")); assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2().getAttr(), is("attr_level2")); assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); table.deleteTable(); @@ -542,20 +527,16 @@ public void immutableSchema_simpleRecordWithList_populatesTimestamps() { table.putItem( SimpleImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) + .id("1") .childList(Arrays.asList( - NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + AutogeneratedTimestampTestModels.SimpleImmutableChild.builder().id("child1").build(), + AutogeneratedTimestampTestModels.SimpleImmutableChild.builder().id("child2").build())) .build()); SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr(), is("attr_level1")); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(0).getAttr(), is("attr_child1")); assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(1).getAttr(), is("attr_child2")); assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); table.deleteTable(); @@ -564,20 +545,19 @@ public void immutableSchema_simpleRecordWithList_populatesTimestamps() { @Test public void immutableSchema_simpleRecordWithSet_populatesTimestamps() { String tableName = getConcreteTableName("immutable-simple-set-table"); - DynamoDbTable table = enhancedClient.table(tableName, - ImmutableTableSchema.create(SimpleImmutableRecordWithSet.class)); + DynamoDbTable table = enhancedClient.table(tableName, + ImmutableTableSchema.create(SimpleImmutableRecordWithSet.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( - SimpleImmutableRecordWithSet + AutogeneratedTimestampTestModels.SimpleImmutableRecordWithSet .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) + .id("1") .childSet(new HashSet<>(Arrays.asList("child1", "child2"))) .build()); - SimpleImmutableRecordWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + AutogeneratedTimestampTestModels.SimpleImmutableRecordWithSet result = table.getItem(r -> r.key(k -> k.partitionValue( + "1"))); - assertThat(result.getAttr(), is("attr_level1")); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); assertThat(result.getChildSet(), hasSize(2)); assertThat(result.getChildSet().contains("child1"), is(true)); @@ -589,22 +569,21 @@ public void immutableSchema_simpleRecordWithSet_populatesTimestamps() { @Test public void immutableSchema_simpleRecordWithMap_populatesTimestamps() { String tableName = getConcreteTableName("immutable-simple-map-table"); - DynamoDbTable table = enhancedClient.table(tableName, - ImmutableTableSchema.create(SimpleImmutableRecordWithMap.class)); + DynamoDbTable table = enhancedClient.table(tableName, + ImmutableTableSchema.create(SimpleImmutableRecordWithMap.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( - SimpleImmutableRecordWithMap.builder() - .id(ID_1) - .attr(ATTR_LEVEL1) - .childMap(new HashMap() {{ - put("child1", "attr_child1"); - put("child2", "attr_child2"); - }}) - .build()); - - SimpleImmutableRecordWithMap result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - - assertThat(result.getAttr(), is("attr_level1")); + AutogeneratedTimestampTestModels.SimpleImmutableRecordWithMap.builder() + .id("1") + .childMap(new HashMap() {{ + put("child1", "attr_child1"); + put("child2", "attr_child2"); + }}) + .build()); + + AutogeneratedTimestampTestModels.SimpleImmutableRecordWithMap result = table.getItem(r -> r.key(k -> k.partitionValue( + "1"))); + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); assertThat(result.getChildMap().size(), is(2)); assertThat(result.getChildMap().get("child1"), is("attr_child1")); @@ -622,17 +601,14 @@ public void immutableSchema_nestedRecordWithList_populatesTimestamps() { table.putItem( NestedImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) - .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList.builder().attr("attr_level2").build()) + .id("1") + .level2(AutogeneratedTimestampTestModels.NestedImmutableChildRecordWithList.builder().build()) .build() ); NestedImmutableRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(level1.getAttr(), is("attr_level1")); assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2().getAttr(), is("attr_level2")); assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); table.deleteTable(); @@ -647,12 +623,10 @@ public void staticSchema_simpleRecordWithList_populatesTimestamps() { table.putItem( new SimpleStaticRecordWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1)); + .setId("1")); SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr(), is("attr_level1")); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); table.deleteTable(); @@ -667,15 +641,12 @@ public void staticSchema_nestedRecordWithList_populatesTimestamps() { table.putItem( new NestedStaticRecordWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) - .setLevel2(new NestedStructureTestModels.NestedStaticChildRecordWithList().setAttr("attr_level2"))); + .setId("1") + .setLevel2(new AutogeneratedTimestampTestModels.NestedStaticChildRecordWithList())); NestedStaticRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(level1.getAttr(), is("attr_level1")); assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2().getAttr(), is("attr_level2")); assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); table.deleteTable(); @@ -690,20 +661,16 @@ public void staticImmutableSchema_simpleRecordWithList_populatesTimestamps() { table.putItem( SimpleImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) + .id("1") .childList(Arrays.asList( - NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + AutogeneratedTimestampTestModels.SimpleImmutableChild.builder().id("child1").build(), + AutogeneratedTimestampTestModels.SimpleImmutableChild.builder().id("child2").build())) .build()); SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr(), is("attr_level1")); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(0).getAttr(), is("attr_child1")); assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(1).getAttr(), is("attr_child2")); assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); table.deleteTable(); @@ -718,16 +685,13 @@ public void staticImmutableSchema_nestedRecordWithList_populatesTimestamps() { table.putItem( NestedImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) - .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList.builder().attr("attr_level2").build()) + .id("1") + .level2(AutogeneratedTimestampTestModels.NestedImmutableChildRecordWithList.builder().build()) .build()); NestedImmutableRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(level1.getAttr(), is("attr_level1")); assertThat(level1.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(level1.getLevel2().getAttr(), is("attr_level2")); assertThat(level1.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); table.deleteTable(); @@ -1071,10 +1035,8 @@ public int hashCode() { */ private static class RecursiveRecord { private String id; - private String name; private Instant parentTimestamp; - private Instant childTimestamp1; - private Instant childTimestamp2; + private Instant childTimestamp; private RecursiveRecord child; public String getId() { @@ -1086,15 +1048,6 @@ public RecursiveRecord setId(String id) { return this; } - public String getName() { - return name; - } - - public RecursiveRecord setName(String name) { - this.name = name; - return this; - } - public Instant getParentTimestamp() { return parentTimestamp; } @@ -1104,21 +1057,12 @@ public RecursiveRecord setParentTimestamp(Instant parentTimestamp) { return this; } - public Instant getChildTimestamp1() { - return childTimestamp1; + public Instant getChildTimestamp() { + return childTimestamp; } - public RecursiveRecord setChildTimestamp1(Instant childTimestamp1) { - this.childTimestamp1 = childTimestamp1; - return this; - } - - public Instant getChildTimestamp2() { - return childTimestamp2; - } - - public RecursiveRecord setChildTimestamp2(Instant childTimestamp2) { - this.childTimestamp2 = childTimestamp2; + public RecursiveRecord setChildTimestamp(Instant childTimestamp) { + this.childTimestamp = childTimestamp; return this; } @@ -1132,25 +1076,23 @@ public RecursiveRecord setChild(RecursiveRecord child) { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { + public final boolean equals(Object o) { + if (!(o instanceof RecursiveRecord)) { return false; } + RecursiveRecord that = (RecursiveRecord) o; - return Objects.equals(id, that.id) && - Objects.equals(name, that.name) && - Objects.equals(parentTimestamp, that.parentTimestamp) && - Objects.equals(childTimestamp1, that.childTimestamp1) && - Objects.equals(childTimestamp2, that.childTimestamp2) && - Objects.equals(child, that.child); + return Objects.equals(id, that.id) && Objects.equals(parentTimestamp, that.parentTimestamp) + && Objects.equals(childTimestamp, that.childTimestamp) && Objects.equals(child, that.child); } @Override public int hashCode() { - return Objects.hash(id, name, parentTimestamp, childTimestamp1, childTimestamp2, child); + int result = Objects.hashCode(id); + result = 31 * result + Objects.hashCode(parentTimestamp); + result = 31 * result + Objects.hashCode(childTimestamp); + result = 31 * result + Objects.hashCode(child); + return result; } } @@ -1311,16 +1253,9 @@ private TableSchema createRecursiveRecordLevel3Schema() { .getter(RecursiveRecord::getId) .setter(RecursiveRecord::setId) .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("name") - .getter(RecursiveRecord::getName) - .setter(RecursiveRecord::setName)) - .addAttribute(Instant.class, a -> a.name("grandchildTimestamp1") - .getter(RecursiveRecord::getChildTimestamp1) - .setter(RecursiveRecord::setChildTimestamp1) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(Instant.class, a -> a.name("grandchildTimestamp2") - .getter(RecursiveRecord::getChildTimestamp2) - .setter(RecursiveRecord::setChildTimestamp2) + .addAttribute(Instant.class, a -> a.name("grandchildTimestamp") + .getter(RecursiveRecord::getChildTimestamp) + .setter(RecursiveRecord::setChildTimestamp) .tags(autoGeneratedTimestampAttribute())) .build(); } @@ -1339,16 +1274,9 @@ private TableSchema createRecursiveRecordLevel2Schema() { .getter(RecursiveRecord::getId) .setter(RecursiveRecord::setId) .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("name") - .getter(RecursiveRecord::getName) - .setter(RecursiveRecord::setName)) - .addAttribute(Instant.class, a -> a.name("childTimestamp1") - .getter(RecursiveRecord::getChildTimestamp1) - .setter(RecursiveRecord::setChildTimestamp1) - .tags(autoGeneratedTimestampAttribute())) - .addAttribute(Instant.class, a -> a.name("childTimestamp2") - .getter(RecursiveRecord::getChildTimestamp2) - .setter(RecursiveRecord::setChildTimestamp2) + .addAttribute(Instant.class, a -> a.name("childTimestamp") + .getter(RecursiveRecord::getChildTimestamp) + .setter(RecursiveRecord::setChildTimestamp) .tags(autoGeneratedTimestampAttribute())) .addAttribute(EnhancedType.documentOf(RecursiveRecord.class, level3Schema), a -> a.name("child") @@ -1371,9 +1299,6 @@ private TableSchema createRecursiveRecordLevel1Schema() { .getter(RecursiveRecord::getId) .setter(RecursiveRecord::setId) .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("name") - .getter(RecursiveRecord::getName) - .setter(RecursiveRecord::setName)) .addAttribute(Instant.class, a -> a.name("parentTimestamp") .getter(RecursiveRecord::getParentTimestamp) .setter(RecursiveRecord::setParentTimestamp) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java index 49d5b0d360f0..21a9dd7e7d15 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java @@ -16,13 +16,11 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.ATTR_LEVEL1; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.ID_1; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedStaticChildRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticImmutableSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticImmutableSchemaForSimpleRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.buildStaticSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.NestedStaticChildRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.buildStaticImmutableSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.buildStaticImmutableSchemaForSimpleRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.buildStaticSchemaForNestedRecordWithList; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.buildStaticSchemaForSimpleRecordWithList; import java.time.Clock; import java.time.Instant; @@ -36,13 +34,13 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedBeanWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.NestedStaticRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleBeanWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedStructureTestModels.SimpleStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.NestedBeanWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.NestedImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.NestedStaticRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.SimpleBeanWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.SimpleImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.SimpleStaticRecordWithList; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.IgnoreNullsMode; @@ -87,24 +85,22 @@ public void beanSchema_simpleRecord_writeIfNotExists_isRespected() { createAndPut("simple-bean-write-if-not-exists", schema, new SimpleBeanWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) + .setId("1") + .setAttr("attr_level1") .setChildList(Arrays.asList( - new NestedStructureTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), - new NestedStructureTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2")))); + new UpdateBehaviorTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), + new UpdateBehaviorTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2")))); - SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); - assertThat(result.getTime()).isEqualTo(MOCKED_INSTANT_NOW); + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getAttr()).isEqualTo("attr_level1"); assertThat(result.getChildList()).hasSize(2); // update with new attr value - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) result.setAttr("updated_level1"); table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - SimpleBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change - assertThat(updated.getTime()).isNotNull().isEqualTo(MOCKED_INSTANT_NOW); // timestamp should update + SimpleBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(updated.getAttr()).isEqualTo("updated_level1"); // should NOT change table.deleteTable(); } @@ -116,12 +112,12 @@ public void beanSchema_nestedRecord_writeIfNotExists_isRespected() { createAndPut("nested-bean-write-if-not-exists", schema, new NestedBeanWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) - .setLevel2(new NestedStructureTestModels.NestedBeanChild().setAttr("attr_level2"))); + .setId("1") + .setAttr("attr_level1") + .setLevel2(new UpdateBehaviorTestModels.NestedBeanChild().setAttr("attr_level2"))); - NestedBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + NestedBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getAttr()).isEqualTo("attr_level1"); assertThat(result.getLevel2()).isNotNull(); assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); @@ -130,8 +126,8 @@ public void beanSchema_nestedRecord_writeIfNotExists_isRespected() { result.getLevel2().setAttr("updated_level2"); table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - NestedBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + NestedBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(updated.getAttr()).isEqualTo("attr_level1"); // should NOT change assertThat(updated.getLevel2().getAttr()).isEqualTo("attr_level2"); // should NOT change table.deleteTable(); @@ -145,26 +141,25 @@ public void immutableSchema_simpleRecord_writeIfNotExists_isRespected() { schema, SimpleImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) + .id("1") + .attr("attr_level1") .childList(Arrays.asList( - NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) .build()); - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); - assertThat(result.getTime()).isEqualTo(MOCKED_INSTANT_NOW); + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getAttr()).isEqualTo("attr_level1"); assertThat(result.getChildList()).hasSize(2); // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() - .id(ID_1).attr("updated_level1") + .id("1").attr("updated_level1") .build(); table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(afterUpdate.getAttr()).isEqualTo("attr_level1"); // should NOT change table.deleteTable(); } @@ -177,25 +172,25 @@ public void immutableSchema_nestedRecord_writeIfNotExists_isRespected() { schema, NestedImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) - .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList + .id("1") + .attr("attr_level1") + .level2(UpdateBehaviorTestModels.NestedImmutableChildRecordWithList .builder().attr("attr_level2").build()) .build()); - NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getAttr()).isEqualTo("attr_level1"); assertThat(result.getLevel2()).isNotNull(); assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() - .id(ID_1).attr("updated_level1") + .id("1").attr("updated_level1") .build(); table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(afterUpdate.getAttr()).isEqualTo("attr_level1"); // should NOT change table.deleteTable(); } @@ -206,19 +201,18 @@ public void staticSchema_simpleRecord_writeIfNotExists_isRespected() { createAndPut("simple-static-write-if-not-exists", buildStaticSchemaForSimpleRecordWithList(), new SimpleStaticRecordWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1)); + .setId("1") + .setAttr("attr_level1")); - SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); - assertThat(result.getTime()).isEqualTo(MOCKED_INSTANT_NOW); + SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getAttr()).isEqualTo("attr_level1"); // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) result.setAttr("updated_level1"); table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - SimpleStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + SimpleStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(updated.getAttr()).isEqualTo("attr_level1"); // should NOT change table.deleteTable(); } @@ -230,12 +224,12 @@ public void staticSchema_nestedRecord_writeIfNotExists_isRespected() { "nested-static-write-if-not-exists", buildStaticSchemaForNestedRecordWithList(), new NestedStaticRecordWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) + .setId("1") + .setAttr("attr_level1") .setLevel2(new NestedStaticChildRecordWithList().setAttr("attr_level2"))); - NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getAttr()).isEqualTo("attr_level1"); assertThat(result.getLevel2()).isNotNull(); assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); @@ -244,8 +238,8 @@ public void staticSchema_nestedRecord_writeIfNotExists_isRespected() { result.getLevel2().setAttr("updated_level2"); table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - NestedStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(updated.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + NestedStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(updated.getAttr()).isEqualTo("attr_level1"); // should NOT change assertThat(updated.getLevel2().getAttr()).isEqualTo("attr_level2"); // should NOT change table.deleteTable(); @@ -258,25 +252,24 @@ public void staticImmutableSchema_simpleRecord_writeIfNotExists_isRespected() { buildStaticImmutableSchemaForSimpleRecordWithList(), SimpleImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) + .id("1") + .attr("attr_level1") .childList(Arrays.asList( - NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) .build()); - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); - assertThat(result.getTime()).isEqualTo(MOCKED_INSTANT_NOW); + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getAttr()).isEqualTo("attr_level1"); // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() - .id(ID_1).attr("updated_level1") + .id("1").attr("updated_level1") .build(); table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(afterUpdate.getAttr()).isEqualTo("attr_level1"); // should NOT change table.deleteTable(); } @@ -288,25 +281,25 @@ public void staticImmutableSchema_nestedRecord_writeIfNotExists_isRespected() { buildStaticImmutableSchemaForNestedRecordWithList(), NestedImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) - .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList + .id("1") + .attr("attr_level1") + .level2(UpdateBehaviorTestModels.NestedImmutableChildRecordWithList .builder().attr("attr_level2").build()) .build()); - NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getAttr()).isEqualTo(ATTR_LEVEL1); + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getAttr()).isEqualTo("attr_level1"); assertThat(result.getLevel2()).isNotNull(); assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() - .id(ID_1).attr("updated_level1") + .id("1").attr("updated_level1") .build(); table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(afterUpdate.getAttr()).isEqualTo(ATTR_LEVEL1); // should NOT change + NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(afterUpdate.getAttr()).isEqualTo("attr_level1"); // should NOT change table.deleteTable(); } @@ -315,11 +308,11 @@ public void staticImmutableSchema_nestedRecord_writeIfNotExists_isRespected() { public void beanSchema_simpleRecord_writeAlways_updatesValues() { TableSchema schema = BeanTableSchema.create(SimpleBeanWithList.class); SimpleBeanWithList initial = new SimpleBeanWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) + .setId("1") + .setAttr("attr_level1") .setChildList(Arrays.asList( - new NestedStructureTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), - new NestedStructureTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2"))); + new UpdateBehaviorTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), + new UpdateBehaviorTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2"))); initial.setId("initial_id"); DynamoDbTable table = createAndPut("simple-bean-write-always", schema, initial); @@ -341,9 +334,9 @@ public void beanSchema_simpleRecord_writeAlways_updatesValues() { public void beanSchema_nestedRecord_writeAlways_updatesValues() { TableSchema schema = BeanTableSchema.create(NestedBeanWithList.class); NestedBeanWithList initial = new NestedBeanWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) - .setLevel2(new NestedStructureTestModels.NestedBeanChild().setAttr("attr_level2")); + .setId("1") + .setAttr("attr_level1") + .setLevel2(new UpdateBehaviorTestModels.NestedBeanChild().setAttr("attr_level2")); initial.setId("initial_id"); DynamoDbTable table = createAndPut("nested-bean-write-always", schema, initial); @@ -367,21 +360,21 @@ public void immutableSchema_simpleRecord_writeAlways_updatesValues() { SimpleImmutableRecordWithList initial = SimpleImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) + .id("1") + .attr("attr_level1") .childList(Arrays.asList( - NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) .build(); DynamoDbTable table = createAndPut("simple-immutable-write-always", schema, initial); - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getId()).isEqualTo(ID_1); + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getId()).isEqualTo("1"); // update with new id - should change (WRITE_ALWAYS is default) SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() - .id("updated_id").attr(ATTR_LEVEL1) + .id("updated_id").attr("attr_level1") .build(); table.updateItem(updated); @@ -398,19 +391,19 @@ public void immutableSchema_nestedRecord_writeAlways_updatesValues() { NestedImmutableRecordWithList initial = NestedImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) - .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList + .id("1") + .attr("attr_level1") + .level2(UpdateBehaviorTestModels.NestedImmutableChildRecordWithList .builder().attr("attr_level2").build()) .build(); DynamoDbTable table = createAndPut("nested-immutable-write-always", schema, initial); - NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getId()).isEqualTo(ID_1); + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getId()).isEqualTo("1"); // update with new id - should change (WRITE_ALWAYS is default) NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() - .id("updated_id").attr(ATTR_LEVEL1) + .id("updated_id").attr("attr_level1") .build(); table.updateItem(updated); @@ -427,11 +420,11 @@ public void staticSchema_simpleRecord_writeAlways_updatesValues() { createAndPut("simple-static-write-always", buildStaticSchemaForSimpleRecordWithList(), new SimpleStaticRecordWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1)); + .setId("1") + .setAttr("attr_level1")); - SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getId()).isEqualTo(ID_1); + SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getId()).isEqualTo("1"); // update id - should change (WRITE_ALWAYS is default) result.setId("updated_id"); @@ -450,12 +443,12 @@ public void staticSchema_nestedRecord_writeAlways_updatesValues() { "nested-static-write-always", buildStaticSchemaForNestedRecordWithList(), new NestedStaticRecordWithList() - .setId(ID_1) - .setAttr(ATTR_LEVEL1) - .setLevel2(new NestedStructureTestModels.NestedStaticChildRecordWithList().setAttr("attr_level2"))); + .setId("1") + .setAttr("attr_level1") + .setLevel2(new UpdateBehaviorTestModels.NestedStaticChildRecordWithList().setAttr("attr_level2"))); - NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getId()).isEqualTo(ID_1); + NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getId()).isEqualTo("1"); // update id - should change (WRITE_ALWAYS is default) result.setId("updated_id"); @@ -475,19 +468,19 @@ public void staticImmutableSchema_simpleRecord_writeAlways_updatesValues() { buildStaticImmutableSchemaForSimpleRecordWithList(), SimpleImmutableRecordWithList .builder() - .id(ID_1) - .attr(ATTR_LEVEL1) + .id("1") + .attr("attr_level1") .childList(Arrays.asList( - NestedStructureTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - NestedStructureTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) + UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), + UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) .build()); - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getId()).isEqualTo(ID_1); + SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getId()).isEqualTo("1"); // update with new id - should change (WRITE_ALWAYS is default) SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() - .id("updated_id").attr(ATTR_LEVEL1) + .id("updated_id").attr("attr_level1") .build(); table.updateItem(updated); @@ -504,18 +497,18 @@ public void staticImmutableSchema_nestedRecord_writeAlways_updatesValues() { createAndPut("nested-static-immutable-write-always", buildStaticImmutableSchemaForNestedRecordWithList(), NestedImmutableRecordWithList.builder() - .id(ID_1) - .attr(ATTR_LEVEL1) - .level2(NestedStructureTestModels.NestedImmutableChildRecordWithList + .id("1") + .attr("attr_level1") + .level2(UpdateBehaviorTestModels.NestedImmutableChildRecordWithList .builder().attr("attr_level2").build()) .build()); - NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue(ID_1))); - assertThat(result.getId()).isEqualTo(ID_1); + NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + assertThat(result.getId()).isEqualTo("1"); // update with new id - should change (WRITE_ALWAYS is default) NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() - .id("updated_id").attr(ATTR_LEVEL1) + .id("updated_id").attr("attr_level1") .build(); table.updateItem(updated); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedStructureTestModels.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java similarity index 75% rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedStructureTestModels.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java index b76248b0061f..8b8e1520f934 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedStructureTestModels.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java @@ -17,7 +17,6 @@ import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; import java.time.Instant; import java.util.Collections; @@ -30,29 +29,22 @@ import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; 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.DynamoDbImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; /** - * Shared models used by various tests for nested structures, auto-generated timestamps, and update behavior testing. This file - * contains models for different schema types (Bean, Immutable, Static, Static-Immutable) with various nesting levels and - * collection types. + * Test models specifically designed for auto-generated timestamp functionality testing. These models focus on the "time" + * attribute with @DynamoDbAutoGeneratedTimestampAttribute annotation and are used by AutoGeneratedTimestampExtensionTest. */ -public final class NestedStructureTestModels { +public final class AutogeneratedTimestampTestModels { - private NestedStructureTestModels() { + private AutogeneratedTimestampTestModels() { } - public static final String ID_1 = "1"; - public static final String ATTR_LEVEL1 = "attr_level1"; - @DynamoDbBean public static class SimpleBeanWithList { private String id; - private String attr; private Instant time; private List childList; @@ -66,16 +58,6 @@ public SimpleBeanWithList setId(String id) { return this; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public SimpleBeanWithList setAttr(String attr) { - this.attr = attr; - return this; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -104,20 +86,19 @@ public boolean equals(Object o) { return false; } SimpleBeanWithList that = (SimpleBeanWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + return Objects.equals(id, that.id) && Objects.equals(time, that.time) && Objects.equals(childList, that.childList); } @Override public int hashCode() { - return Objects.hash(id, attr, time, childList); + return Objects.hash(id, time, childList); } } @DynamoDbBean public static class SimpleBeanWithSet { private String id; - private String attr; private Instant time; private Set childSet; @@ -131,16 +112,6 @@ public SimpleBeanWithSet setId(String id) { return this; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public SimpleBeanWithSet setAttr(String attr) { - this.attr = attr; - return this; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -164,7 +135,6 @@ public SimpleBeanWithSet setChildSet(Set childSet) { @DynamoDbBean public static class SimpleBeanWithMap { private String id; - private String attr; private Instant time; private Map childMap; @@ -178,16 +148,6 @@ public SimpleBeanWithMap setId(String id) { return this; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public SimpleBeanWithMap setAttr(String attr) { - this.attr = attr; - return this; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -211,7 +171,6 @@ public SimpleBeanWithMap setChildMap(Map childMap) { @DynamoDbBean public static class SimpleBeanChild { private String id; - private String attr; private Instant time; @DynamoDbPartitionKey @@ -224,16 +183,6 @@ public SimpleBeanChild setId(String id) { return this; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public SimpleBeanChild setAttr(String attr) { - this.attr = attr; - return this; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -253,19 +202,18 @@ public boolean equals(Object o) { return false; } SimpleBeanChild that = (SimpleBeanChild) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + return Objects.equals(id, that.id) && Objects.equals(time, that.time); } @Override public int hashCode() { - return Objects.hash(id, attr, time); + return Objects.hash(id, time); } } @DynamoDbBean public static class NestedBeanWithList { private String id; - private String attr; private Instant time; private NestedBeanChild level2; @@ -279,16 +227,6 @@ public NestedBeanWithList setId(String id) { return this; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public NestedBeanWithList setAttr(String attr) { - this.attr = attr; - return this; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -317,31 +255,20 @@ public boolean equals(Object o) { return false; } NestedBeanWithList that = (NestedBeanWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + return Objects.equals(id, that.id) && Objects.equals(time, that.time) && Objects.equals(level2, that.level2); } @Override public int hashCode() { - return Objects.hash(id, attr, time, level2); + return Objects.hash(id, time, level2); } } @DynamoDbBean public static class NestedBeanChild { - private String attr; private Instant time; - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public NestedBeanChild setAttr(String attr) { - this.attr = attr; - return this; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -361,25 +288,23 @@ public boolean equals(Object o) { return false; } NestedBeanChild that = (NestedBeanChild) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + return Objects.equals(time, that.time); } @Override public int hashCode() { - return Objects.hash(attr, time); + return Objects.hash(time); } } @DynamoDbImmutable(builder = SimpleImmutableRecordWithList.Builder.class) public static final class SimpleImmutableRecordWithList { private final String id; - private final String attr; private final Instant time; private final List childList; private SimpleImmutableRecordWithList(Builder b) { this.id = b.id; - this.attr = b.attr; this.time = b.time; this.childList = b.childList; } @@ -389,11 +314,6 @@ public String getId() { return id; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -409,7 +329,6 @@ public static Builder builder() { public static final class Builder { private String id; - private String attr; private Instant time; private List childList; @@ -418,11 +337,6 @@ public Builder id(String id) { return this; } - public Builder attr(String attr) { - this.attr = attr; - return this; - } - public Builder time(Instant time) { this.time = time; return this; @@ -447,25 +361,23 @@ public boolean equals(Object o) { return false; } SimpleImmutableRecordWithList that = (SimpleImmutableRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + return Objects.equals(id, that.id) && Objects.equals(time, that.time) && Objects.equals(childList, that.childList); } @Override public int hashCode() { - return Objects.hash(id, attr, time, childList); + return Objects.hash(id, time, childList); } } @DynamoDbImmutable(builder = SimpleImmutableChild.Builder.class) public static final class SimpleImmutableChild { private final String id; - private final String attr; private final Instant time; private SimpleImmutableChild(Builder b) { this.id = b.id; - this.attr = b.attr; this.time = b.time; } @@ -474,11 +386,6 @@ public String getId() { return id; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -490,7 +397,6 @@ public static Builder builder() { public static final class Builder { private String id; - private String attr; private Instant time; public Builder id(String id) { @@ -498,11 +404,6 @@ public Builder id(String id) { return this; } - public Builder attr(String attr) { - this.attr = attr; - return this; - } - public Builder time(Instant time) { this.time = time; return this; @@ -522,25 +423,23 @@ public boolean equals(Object o) { return false; } SimpleImmutableChild that = (SimpleImmutableChild) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + return Objects.equals(id, that.id) && Objects.equals(time, that.time); } @Override public int hashCode() { - return Objects.hash(id, attr, time); + return Objects.hash(id, time); } } @DynamoDbImmutable(builder = SimpleImmutableRecordWithSet.Builder.class) public static final class SimpleImmutableRecordWithSet { private final String id; - private final String attr; private final Instant time; private final Set childSet; private SimpleImmutableRecordWithSet(Builder b) { this.id = b.id; - this.attr = b.attr; this.time = b.time; this.childSet = b.childSet; } @@ -550,11 +449,6 @@ public String getId() { return id; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -570,7 +464,6 @@ public static Builder builder() { public static final class Builder { private String id; - private String attr; private Instant time; private Set childSet; @@ -579,11 +472,6 @@ public Builder id(String id) { return this; } - public Builder attr(String attr) { - this.attr = attr; - return this; - } - public Builder time(Instant time) { this.time = time; return this; @@ -603,13 +491,11 @@ public SimpleImmutableRecordWithSet build() { @DynamoDbImmutable(builder = SimpleImmutableRecordWithMap.Builder.class) public static final class SimpleImmutableRecordWithMap { private final String id; - private final String attr; private final Instant time; private final Map childMap; private SimpleImmutableRecordWithMap(Builder b) { this.id = b.id; - this.attr = b.attr; this.time = b.time; this.childMap = b.childMap; } @@ -619,11 +505,6 @@ public String getId() { return id; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -639,7 +520,6 @@ public static Builder builder() { public static final class Builder { private String id; - private String attr; private Instant time; private Map childMap; @@ -648,11 +528,6 @@ public Builder id(String id) { return this; } - public Builder attr(String attr) { - this.attr = attr; - return this; - } - public Builder time(Instant time) { this.time = time; return this; @@ -672,13 +547,11 @@ public SimpleImmutableRecordWithMap build() { @DynamoDbImmutable(builder = NestedImmutableRecordWithList.Builder.class) public static final class NestedImmutableRecordWithList { private final String id; - private final String attr; private final Instant time; private final NestedImmutableChildRecordWithList level2; private NestedImmutableRecordWithList(Builder b) { this.id = b.id; - this.attr = b.attr; this.time = b.time; this.level2 = b.level2; } @@ -688,11 +561,6 @@ public String getId() { return id; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -708,7 +576,6 @@ public static Builder builder() { public static final class Builder { private String id; - private String attr; private Instant time; private NestedImmutableChildRecordWithList level2; @@ -717,11 +584,6 @@ public Builder id(String id) { return this; } - public Builder attr(String attr) { - this.attr = attr; - return this; - } - public Builder time(Instant time) { this.time = time; return this; @@ -746,31 +608,24 @@ public boolean equals(Object o) { return false; } NestedImmutableRecordWithList that = (NestedImmutableRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + return Objects.equals(id, that.id) && Objects.equals(time, that.time) && Objects.equals(level2, that.level2); } @Override public int hashCode() { - return Objects.hash(id, attr, time, level2); + return Objects.hash(id, time, level2); } } @DynamoDbImmutable(builder = NestedImmutableChildRecordWithList.Builder.class) public static final class NestedImmutableChildRecordWithList { - private final String attr; private final Instant time; private NestedImmutableChildRecordWithList(Builder b) { - this.attr = b.attr; this.time = b.time; } - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { return time; @@ -781,14 +636,8 @@ public static Builder builder() { } public static final class Builder { - private String attr; private Instant time; - public Builder attr(String attr) { - this.attr = attr; - return this; - } - public Builder time(Instant time) { this.time = time; return this; @@ -808,18 +657,17 @@ public boolean equals(Object o) { return false; } NestedImmutableChildRecordWithList that = (NestedImmutableChildRecordWithList) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + return Objects.equals(time, that.time); } @Override public int hashCode() { - return Objects.hash(attr, time); + return Objects.hash(time); } } public static class SimpleStaticRecordWithList { private String id; - private String attr; private Instant time; public String getId() { @@ -831,15 +679,6 @@ public SimpleStaticRecordWithList setId(String id) { return this; } - public String getAttr() { - return attr; - } - - public SimpleStaticRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - public Instant getTime() { return time; } @@ -858,18 +697,17 @@ public boolean equals(Object o) { return false; } SimpleStaticRecordWithList that = (SimpleStaticRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + return Objects.equals(id, that.id) && Objects.equals(time, that.time); } @Override public int hashCode() { - return Objects.hash(id, attr, time); + return Objects.hash(id, time); } } public static class NestedStaticRecordWithList { private String id; - private String attr; private Instant time; private NestedStaticChildRecordWithList level2; @@ -882,15 +720,6 @@ public NestedStaticRecordWithList setId(String id) { return this; } - public String getAttr() { - return attr; - } - - public NestedStaticRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - public Instant getTime() { return time; } @@ -918,29 +747,19 @@ public boolean equals(Object o) { return false; } NestedStaticRecordWithList that = (NestedStaticRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && + return Objects.equals(id, that.id) && Objects.equals(time, that.time) && Objects.equals(level2, that.level2); } @Override public int hashCode() { - return Objects.hash(id, attr, time, level2); + return Objects.hash(id, time, level2); } } public static class NestedStaticChildRecordWithList { - private String attr; private Instant time; - public String getAttr() { - return attr; - } - - public NestedStaticChildRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - public Instant getTime() { return time; } @@ -959,12 +778,12 @@ public boolean equals(Object o) { return false; } NestedStaticChildRecordWithList that = (NestedStaticChildRecordWithList) o; - return Objects.equals(attr, that.attr) && Objects.equals(time, that.time); + return Objects.equals(time, that.time); } @Override public int hashCode() { - return Objects.hash(attr, time); + return Objects.hash(time); } } @@ -975,10 +794,6 @@ public static TableSchema buildStaticSchemaForSimple .getter(SimpleStaticRecordWithList::getId) .setter(SimpleStaticRecordWithList::setId) .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(SimpleStaticRecordWithList::getAttr) - .setter(SimpleStaticRecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) .addAttribute(Instant.class, a -> a.name("time") .getter(SimpleStaticRecordWithList::getTime) .setter(SimpleStaticRecordWithList::setTime) @@ -990,10 +805,6 @@ public static TableSchema buildStaticSchemaForNested TableSchema level2Schema = StaticTableSchema.builder(NestedStaticChildRecordWithList.class) .newItemSupplier(NestedStaticChildRecordWithList::new) - .addAttribute(String.class, a -> a.name("attr") - .getter(NestedStaticChildRecordWithList::getAttr) - .setter(NestedStaticChildRecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) .addAttribute(Instant.class, a -> a.name("time") .getter(NestedStaticChildRecordWithList::getTime) .setter(NestedStaticChildRecordWithList::setTime) @@ -1006,10 +817,6 @@ public static TableSchema buildStaticSchemaForNested .getter(NestedStaticRecordWithList::getId) .setter(NestedStaticRecordWithList::setId) .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(NestedStaticRecordWithList::getAttr) - .setter(NestedStaticRecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) .addAttribute(Instant.class, a -> a.name("time") .getter(NestedStaticRecordWithList::getTime) .setter(NestedStaticRecordWithList::setTime) @@ -1023,34 +830,28 @@ public static TableSchema buildStaticSchemaForNested public static TableSchema buildStaticImmutableSchemaForSimpleRecordWithList() { TableSchema childSchema = - StaticImmutableTableSchema.builder(SimpleImmutableChild.class, SimpleImmutableChild.Builder.class) + StaticImmutableTableSchema.builder(SimpleImmutableChild.class, + SimpleImmutableChild.Builder.class) .newItemBuilder(SimpleImmutableChild::builder, SimpleImmutableChild.Builder::build) .addAttribute(String.class, a -> a.name("id") .getter(SimpleImmutableChild::getId) .setter(SimpleImmutableChild.Builder::id) .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(SimpleImmutableChild::getAttr) - .setter(SimpleImmutableChild.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) .addAttribute(Instant.class, a -> a.name("time") .getter(SimpleImmutableChild::getTime) .setter(SimpleImmutableChild.Builder::time) .tags(autoGeneratedTimestampAttribute())) .build(); - return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithList.class, SimpleImmutableRecordWithList.Builder.class) + return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithList.class, + SimpleImmutableRecordWithList.Builder.class) .newItemBuilder(SimpleImmutableRecordWithList::builder, SimpleImmutableRecordWithList.Builder::build) .addAttribute(String.class, a -> a.name("id") .getter(SimpleImmutableRecordWithList::getId) .setter(SimpleImmutableRecordWithList.Builder::id) .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(SimpleImmutableRecordWithList::getAttr) - .setter(SimpleImmutableRecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) .addAttribute(Instant.class, a -> a.name("time") .getter(SimpleImmutableRecordWithList::getTime) .setter(SimpleImmutableRecordWithList.Builder::time) @@ -1069,10 +870,6 @@ public static TableSchema buildStaticImmutableSch NestedImmutableChildRecordWithList.Builder.class) .newItemBuilder(NestedImmutableChildRecordWithList::builder, NestedImmutableChildRecordWithList.Builder::build) - .addAttribute(String.class, a -> a.name("attr") - .getter(NestedImmutableChildRecordWithList::getAttr) - .setter(NestedImmutableChildRecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) .addAttribute(Instant.class, a -> a.name("time") .getter(NestedImmutableChildRecordWithList::getTime) .setter(NestedImmutableChildRecordWithList.Builder::time) @@ -1087,10 +884,6 @@ public static TableSchema buildStaticImmutableSch .getter(NestedImmutableRecordWithList::getId) .setter(NestedImmutableRecordWithList.Builder::id) .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(NestedImmutableRecordWithList::getAttr) - .setter(NestedImmutableRecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) .addAttribute(Instant.class, a -> a.name("time") .getter(NestedImmutableRecordWithList::getTime) .setter(NestedImmutableRecordWithList.Builder::time) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UpdateBehaviorTestModels.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UpdateBehaviorTestModels.java new file mode 100644 index 000000000000..71f12eef97b3 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UpdateBehaviorTestModels.java @@ -0,0 +1,715 @@ +/* + * 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 static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; +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.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; + +/** + * Test models specifically designed for update behavior functionality testing. These models focus on the "attr" attribute + * annotated with @DynamoDbUpdateBehavior annotation and are used by NestedUpdateBehaviorTest. + */ +public final class UpdateBehaviorTestModels { + + private UpdateBehaviorTestModels() { + } + + @DynamoDbBean + public static class SimpleBeanWithList { + private String id; + private String attr; + private List childList; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanWithList setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public SimpleBeanWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public List getChildList() { + return childList; + } + + public SimpleBeanWithList setChildList(List childList) { + this.childList = childList; + return this; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof SimpleBeanWithList)) { + return false; + } + + SimpleBeanWithList that = (SimpleBeanWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(childList, that.childList); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + Objects.hashCode(attr); + result = 31 * result + Objects.hashCode(childList); + return result; + } + } + + @DynamoDbBean + public static class SimpleBeanChild { + private String id; + private String attr; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public SimpleBeanChild setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public SimpleBeanChild setAttr(String attr) { + this.attr = attr; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleBeanChild that = (SimpleBeanChild) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr); + } + } + + @DynamoDbBean + public static class NestedBeanWithList { + private String id; + private String attr; + private NestedBeanChild level2; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public NestedBeanWithList setId(String id) { + this.id = id; + return this; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public NestedBeanWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public NestedBeanChild getLevel2() { + return level2; + } + + public NestedBeanWithList setLevel2(NestedBeanChild level2) { + this.level2 = level2; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedBeanWithList that = (NestedBeanWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) + && Objects.equals(level2, that.level2); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, level2); + } + } + + @DynamoDbBean + public static class NestedBeanChild { + private String attr; + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public NestedBeanChild setAttr(String attr) { + this.attr = attr; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedBeanChild that = (NestedBeanChild) o; + return Objects.equals(attr, that.attr); + } + + @Override + public int hashCode() { + return Objects.hash(attr + ); + } + } + + @DynamoDbImmutable(builder = SimpleImmutableRecordWithList.Builder.class) + public static final class SimpleImmutableRecordWithList { + private final String id; + private final String attr; + private final List childList; + + private SimpleImmutableRecordWithList(Builder b) { + this.id = b.id; + this.attr = b.attr; + this.childList = b.childList; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public List getChildList() { + return childList == null ? null : Collections.unmodifiableList(childList); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private List childList; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder childList(List childList) { + this.childList = childList; + return this; + } + + public SimpleImmutableRecordWithList build() { + return new SimpleImmutableRecordWithList(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleImmutableRecordWithList that = (SimpleImmutableRecordWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) + && Objects.equals(childList, that.childList); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, childList); + } + } + + @DynamoDbImmutable(builder = SimpleImmutableChild.Builder.class) + public static final class SimpleImmutableChild { + private final String id; + private final String attr; + + private SimpleImmutableChild(Builder b) { + this.id = b.id; + this.attr = b.attr; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public SimpleImmutableChild build() { + return new SimpleImmutableChild(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleImmutableChild that = (SimpleImmutableChild) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr); + } + } + + @DynamoDbImmutable(builder = NestedImmutableRecordWithList.Builder.class) + public static final class NestedImmutableRecordWithList { + private final String id; + private final String attr; + private final NestedImmutableChildRecordWithList level2; + + private NestedImmutableRecordWithList(Builder b) { + this.id = b.id; + this.attr = b.attr; + + this.level2 = b.level2; + } + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public NestedImmutableChildRecordWithList getLevel2() { + return level2; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String attr; + private NestedImmutableChildRecordWithList level2; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public Builder level2(NestedImmutableChildRecordWithList level2) { + this.level2 = level2; + return this; + } + + public NestedImmutableRecordWithList build() { + return new NestedImmutableRecordWithList(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedImmutableRecordWithList that = (NestedImmutableRecordWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) + && Objects.equals(level2, that.level2); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, level2); + } + } + + @DynamoDbImmutable(builder = NestedImmutableChildRecordWithList.Builder.class) + public static final class NestedImmutableChildRecordWithList { + private final String attr; + + private NestedImmutableChildRecordWithList(Builder b) { + this.attr = b.attr; + + } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public String getAttr() { + return attr; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String attr; + + public Builder attr(String attr) { + this.attr = attr; + return this; + } + + public NestedImmutableChildRecordWithList build() { + return new NestedImmutableChildRecordWithList(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedImmutableChildRecordWithList that = (NestedImmutableChildRecordWithList) o; + return Objects.equals(attr, that.attr); + } + + @Override + public int hashCode() { + return Objects.hash(attr); + } + } + + public static class SimpleStaticRecordWithList { + private String id; + private String attr; + + public String getId() { + return id; + } + + public SimpleStaticRecordWithList setId(String id) { + this.id = id; + return this; + } + + public String getAttr() { + return attr; + } + + public SimpleStaticRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleStaticRecordWithList that = (SimpleStaticRecordWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr); + } + } + + public static class NestedStaticRecordWithList { + private String id; + private String attr; + private NestedStaticChildRecordWithList level2; + + public String getId() { + return id; + } + + public NestedStaticRecordWithList setId(String id) { + this.id = id; + return this; + } + + public String getAttr() { + return attr; + } + + public NestedStaticRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + public NestedStaticChildRecordWithList getLevel2() { + return level2; + } + + public NestedStaticRecordWithList setLevel2(NestedStaticChildRecordWithList level2) { + this.level2 = level2; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedStaticRecordWithList that = (NestedStaticRecordWithList) o; + return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) + && Objects.equals(level2, that.level2); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr, level2); + } + } + + public static class NestedStaticChildRecordWithList { + private String attr; + + public String getAttr() { + return attr; + } + + public NestedStaticChildRecordWithList setAttr(String attr) { + this.attr = attr; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedStaticChildRecordWithList that = (NestedStaticChildRecordWithList) o; + return Objects.equals(attr, that.attr); + } + + @Override + public int hashCode() { + return Objects.hash(attr); + } + } + + public static TableSchema buildStaticSchemaForSimpleRecordWithList() { + return StaticTableSchema.builder(SimpleStaticRecordWithList.class) + .newItemSupplier(SimpleStaticRecordWithList::new) + .addAttribute(String.class, a -> a.name("id") + .getter(SimpleStaticRecordWithList::getId) + .setter(SimpleStaticRecordWithList::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(SimpleStaticRecordWithList::getAttr) + .setter(SimpleStaticRecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .build(); + } + + public static TableSchema buildStaticSchemaForNestedRecordWithList() { + TableSchema level2Schema = + StaticTableSchema.builder(NestedStaticChildRecordWithList.class) + .newItemSupplier(NestedStaticChildRecordWithList::new) + .addAttribute(String.class, a -> a.name("attr") + .getter(NestedStaticChildRecordWithList::getAttr) + .setter(NestedStaticChildRecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .build(); + + return StaticTableSchema.builder(NestedStaticRecordWithList.class) + .newItemSupplier(NestedStaticRecordWithList::new) + .addAttribute(String.class, a -> a.name("id") + .getter(NestedStaticRecordWithList::getId) + .setter(NestedStaticRecordWithList::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(NestedStaticRecordWithList::getAttr) + .setter(NestedStaticRecordWithList::setAttr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(EnhancedType.documentOf(NestedStaticChildRecordWithList.class, level2Schema), + a -> a.name("level2") + .getter(NestedStaticRecordWithList::getLevel2) + .setter(NestedStaticRecordWithList::setLevel2)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForSimpleRecordWithList() { + TableSchema childSchema = + StaticImmutableTableSchema.builder(SimpleImmutableChild.class, + SimpleImmutableChild.Builder.class) + .newItemBuilder(SimpleImmutableChild::builder, + SimpleImmutableChild.Builder::build) + .addAttribute(String.class, a -> a.name("id") + .getter(SimpleImmutableChild::getId) + .setter(SimpleImmutableChild.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(SimpleImmutableChild::getAttr) + .setter(SimpleImmutableChild.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .build(); + + return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithList.class, + SimpleImmutableRecordWithList.Builder.class) + .newItemBuilder(SimpleImmutableRecordWithList::builder, + SimpleImmutableRecordWithList.Builder::build) + .addAttribute(String.class, a -> a.name("id") + .getter(SimpleImmutableRecordWithList::getId) + .setter(SimpleImmutableRecordWithList.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(SimpleImmutableRecordWithList::getAttr) + .setter(SimpleImmutableRecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(EnhancedType.listOf(EnhancedType.documentOf(SimpleImmutableChild.class, + childSchema)), + a -> a.name("childList") + .getter(SimpleImmutableRecordWithList::getChildList) + .setter(SimpleImmutableRecordWithList.Builder::childList)) + .build(); + } + + public static TableSchema buildStaticImmutableSchemaForNestedRecordWithList() { + TableSchema level2Schema = + StaticImmutableTableSchema.builder(NestedImmutableChildRecordWithList.class, + NestedImmutableChildRecordWithList.Builder.class) + .newItemBuilder(NestedImmutableChildRecordWithList::builder, + NestedImmutableChildRecordWithList.Builder::build) + .addAttribute(String.class, a -> a.name("attr") + .getter(NestedImmutableChildRecordWithList::getAttr) + .setter(NestedImmutableChildRecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .build(); + + return StaticImmutableTableSchema.builder(NestedImmutableRecordWithList.class, + NestedImmutableRecordWithList.Builder.class) + .newItemBuilder(NestedImmutableRecordWithList::builder, + NestedImmutableRecordWithList.Builder::build) + .addAttribute(String.class, a -> a.name("id") + .getter(NestedImmutableRecordWithList::getId) + .setter(NestedImmutableRecordWithList.Builder::id) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("attr") + .getter(NestedImmutableRecordWithList::getAttr) + .setter(NestedImmutableRecordWithList.Builder::attr) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .addAttribute(EnhancedType.documentOf(NestedImmutableChildRecordWithList.class, + level2Schema), + a -> a.name("level2") + .getter(NestedImmutableRecordWithList::getLevel2) + .setter(NestedImmutableRecordWithList.Builder::level2)) + .build(); + } +} \ No newline at end of file From 772e2f31ae7710af569c4a7bfeafb17812d306ea Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 4 Feb 2026 05:37:06 +0200 Subject: [PATCH 09/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- .../dynamodb/functionaltests/NestedUpdateBehaviorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java index 21a9dd7e7d15..d8d2989702d3 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java @@ -100,7 +100,7 @@ public void beanSchema_simpleRecord_writeIfNotExists_isRespected() { table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); SimpleBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(updated.getAttr()).isEqualTo("updated_level1"); // should NOT change + assertThat(updated.getAttr()).isEqualTo("attr_level1"); // should NOT change table.deleteTable(); } From 55bcc069fdde16f73eee03daa2c7a58b6256868a Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 4 Feb 2026 06:01:43 +0200 Subject: [PATCH 10/18] Support AutoGeneratedTimestamp and UpdateBehavior annotations in nested objects --- .../extensions/NestedRecordUtilsTest.java | 4 +- .../AutoGeneratedTimestampExtensionTest.java | 38 ++++----- .../functionaltests/UpdateBehaviorTest.java | 79 ++++++++++--------- ...l.java => InvalidNestedAttributeBean.java} | 28 +++---- ...java => InvalidNestedAttributeRecord.java} | 28 +++---- ...oot.java => InvalidRootAttributeBean.java} | 8 +- ...t.java => InvalidRootAttributeRecord.java} | 8 +- .../NestedRecordWithUpdateBehavior.java | 6 +- .../models/RecordWithUpdateBehaviors.java | 6 +- ...Element.java => TimestampListElement.java} | 4 +- 10 files changed, 106 insertions(+), 103 deletions(-) rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/{BeanWithInvalidAttributeNameOnNestedLevel.java => InvalidNestedAttributeBean.java} (65%) rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/{RecordWithInvalidAttributeNameOnNestedLevel.java => InvalidNestedAttributeRecord.java} (58%) rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/{BeanWithInvalidAttributeNameOnRoot.java => InvalidRootAttributeBean.java} (86%) rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/{RecordWithInvalidAttributeNameOnRoot.java => InvalidRootAttributeRecord.java} (81%) rename services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/{NestedRecordListElement.java => TimestampListElement.java} (97%) 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 index c666b2644a8b..8855f73a719c 100644 --- 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 @@ -23,7 +23,7 @@ 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.TimestampListElement; 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; @@ -37,7 +37,7 @@ public void getTableSchemaForListElement_shouldReturnElementSchema() { TableSchema childSchema = getTableSchemaForListElement(parentSchema, "nestedRecordList"); Assertions.assertNotNull(childSchema); - Assertions.assertEquals(TableSchema.fromBean(NestedRecordListElement.class), childSchema); + Assertions.assertEquals(TableSchema.fromBean(TimestampListElement.class), childSchema); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java index 1b5bc3f38f5a..c9dd1d9bb7e5 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java @@ -63,8 +63,8 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithMap; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithSet; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleStaticRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnNestedLevel; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithInvalidAttributeNameOnRoot; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InvalidNestedAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InvalidRootAttributeRecord; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; @@ -724,17 +724,17 @@ public void autogenerateTimestamps_onRootAttributeWithReservedMarker_throwsExcep + "'_NESTED_ATTR_UPDATE_' and is not allowed."); StaticTableSchema - .builder(RecordWithInvalidAttributeNameOnRoot.class) - .newItemSupplier(RecordWithInvalidAttributeNameOnRoot::new) + .builder(InvalidRootAttributeRecord.class) + .newItemSupplier(InvalidRootAttributeRecord::new) .addAttribute(String.class, a -> a.name("id") - .getter(RecordWithInvalidAttributeNameOnRoot::getId) - .setter(RecordWithInvalidAttributeNameOnRoot::setId) + .getter(InvalidRootAttributeRecord::getId) + .setter(InvalidRootAttributeRecord::setId) .tags(primaryPartitionKey())) .addAttribute(Instant.class, a -> a.name("attr_NESTED_ATTR_UPDATE_") - .getter(RecordWithInvalidAttributeNameOnRoot::getAttr_NESTED_ATTR_UPDATE_) - .setter(RecordWithInvalidAttributeNameOnRoot::setAttr_NESTED_ATTR_UPDATE_) + .getter(InvalidRootAttributeRecord::getAttr_NESTED_ATTR_UPDATE_) + .setter(InvalidRootAttributeRecord::setAttr_NESTED_ATTR_UPDATE_) .tags(autoGeneratedTimestampAttribute())) .build(); } @@ -746,29 +746,29 @@ public void autogenerateTimestamps_onNestedAttributeWithReservedMarker_throwsExc + "'_NESTED_ATTR_UPDATE_' and is not allowed."); StaticTableSchema - .builder(RecordWithInvalidAttributeNameOnNestedLevel.class) - .newItemSupplier(RecordWithInvalidAttributeNameOnNestedLevel::new) + .builder(InvalidNestedAttributeRecord.class) + .newItemSupplier(InvalidNestedAttributeRecord::new) .addAttribute( String.class, a -> a.name("id") - .getter(RecordWithInvalidAttributeNameOnNestedLevel::getId) - .setter(RecordWithInvalidAttributeNameOnNestedLevel::setId) + .getter(InvalidNestedAttributeRecord::getId) + .setter(InvalidNestedAttributeRecord::setId) .tags(primaryPartitionKey())) .addAttribute( EnhancedType.documentOf( - RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute.class, + InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild.class, StaticTableSchema - .builder(RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute.class) - .newItemSupplier(RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute::new) + .builder(InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild.class) + .newItemSupplier(InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild::new) .addAttribute(Instant.class, a -> a.name("childAttr_NESTED_ATTR_UPDATE_") - .getter(RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute::getAttr_NESTED_ATTR_UPDATE_) - .setter(RecordWithInvalidAttributeNameOnNestedLevel.RecordWithReservedMarkerNestedChildAttribute::setAttr_NESTED_ATTR_UPDATE_) + .getter(InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild::getAttr_NESTED_ATTR_UPDATE_) + .setter(InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild::setAttr_NESTED_ATTR_UPDATE_) .tags(autoGeneratedTimestampAttribute())) .build()), a -> a.name("nestedChildAttribute") - .getter(RecordWithInvalidAttributeNameOnNestedLevel::getNestedChildAttribute) - .setter(RecordWithInvalidAttributeNameOnNestedLevel::setNestedChildAttribute)) + .getter(InvalidNestedAttributeRecord::getNestedChildAttribute) + .setter(InvalidNestedAttributeRecord::setNestedChildAttribute)) .build(); } 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 e40c9bc7045c..a99c4b266e7a 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 @@ -3,7 +3,7 @@ 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 static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.BeanWithInvalidAttributeNameOnNestedLevel.BeanChildWithInvalidAttributeNameOnNestedLevel; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InvalidNestedAttributeBean.InvalidNestedAttributeChild; import com.google.common.collect.ImmutableList; import java.time.Instant; @@ -21,13 +21,13 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.BeanWithInvalidAttributeNameOnNestedLevel; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.BeanWithInvalidAttributeNameOnRoot; 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.InvalidNestedAttributeBean; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InvalidRootAttributeBean; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedRecordWithUpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.TimestampListElement; import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; import software.amazon.awssdk.enhanced.dynamodb.model.IgnoreNullsMode; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -43,33 +43,33 @@ public class UpdateBehaviorTest extends LocalDynamoDbSyncTestBase { private static final String TEST_ATTRIBUTE = "testAttribute"; private static final TableSchema TABLE_SCHEMA = - TableSchema.fromClass(RecordWithUpdateBehaviors.class); - + TableSchema.fromClass(RecordWithUpdateBehaviors.class); + private static final TableSchema TABLE_SCHEMA_FLATTEN_RECORD = TableSchema.fromClass(FlattenRecord.class); - private static final TableSchema TABLE_SCHEMA_INVALID_ATTR_NAME_ON_ROOT = - TableSchema.fromClass(BeanWithInvalidAttributeNameOnRoot.class); + private static final TableSchema TABLE_SCHEMA_INVALID_ATTR_NAME_ON_ROOT = + TableSchema.fromClass(InvalidRootAttributeBean.class); - private static final TableSchema TABLE_SCHEMA_INVALID_ATTR_NAME_ON_NESTED_LEVEL = - TableSchema.fromClass(BeanWithInvalidAttributeNameOnNestedLevel.class); + private static final TableSchema TABLE_SCHEMA_INVALID_ATTR_NAME_ON_NESTED_LEVEL = + TableSchema.fromClass(InvalidNestedAttributeBean.class); private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() - .dynamoDbClient(getDynamoDbClient()).extensions( + .dynamoDbClient(getDynamoDbClient()).extensions( Stream.concat(ExtensionResolver.defaultExtensions().stream(), Stream.of(AutoGeneratedTimestampRecordExtension.create())).collect(Collectors.toList())) - .build(); + .build(); private final DynamoDbTable mappedTable = - enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); private final DynamoDbTable flattenedMappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_FLATTEN_RECORD); - private final DynamoDbTable beanWithInvalidRootAttrNameMappedTable = + private final DynamoDbTable beanWithInvalidRootAttrNameMappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_INVALID_ATTR_NAME_ON_ROOT); - private final DynamoDbTable beanWithInvalidNestedAttrNameMappedTable = + private final DynamoDbTable beanWithInvalidNestedAttrNameMappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_INVALID_ATTR_NAME_ON_NESTED_LEVEL); @Rule @@ -297,7 +297,8 @@ public void when_updatingNestedObjectWithSingleLevel_default_mode_update_newMapC persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, currentTime); + verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, + currentTime); assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isAfter(nestedCreatedTime); assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isAfter(nestedUpdatedTime); } @@ -326,7 +327,8 @@ public void when_updatingNestedObjectWithSingleLevel_with_no_mode_update_newMapC RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, currentTime); + verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, + currentTime); } @Test @@ -334,10 +336,10 @@ public void when_updatingNestedObjectList_no_matter_mode_update_newListCreated_w Instant currentTime = Instant.now().minusMillis(1); NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); nestedRecord.setNestedUpdatedTimeAttribute(null); - NestedRecordListElement firstElement = new NestedRecordListElement(); + TimestampListElement firstElement = new TimestampListElement(); firstElement.setId("id1"); firstElement.setAttribute("attr1"); - NestedRecordListElement secondElement = new NestedRecordListElement(); + TimestampListElement secondElement = new TimestampListElement(); secondElement.setId("id2"); secondElement.setAttribute("attr2"); nestedRecord.setNestedRecordList(ImmutableList.of(firstElement, secondElement)); @@ -351,7 +353,7 @@ public void when_updatingNestedObjectList_no_matter_mode_update_newListCreated_w RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - List nestedRecordList = persistedRecord.getNestedRecord().getNestedRecordList(); + List nestedRecordList = persistedRecord.getNestedRecord().getNestedRecordList(); Instant firstOperationTime = nestedRecordList.get(0).getTimeAttributeElement(); assertThat(persistedRecord.getNestedRecordList().get(0).getTimeAttributeElement()).isAfter(currentTime); @@ -590,8 +592,9 @@ public void when_updatingNestedObjectWithMultipleLevels_default_mode_existingInf RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - verifyMultipleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), outerNestedCounter, innerNestedCounter, null, - currentTime); + verifyMultipleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), outerNestedCounter, + innerNestedCounter, null, + currentTime); } @Test @@ -677,15 +680,15 @@ public void when_updatingNestedObjectWithSingleLevelFlattened_existingInformatio FlattenRecord flattenRecord = new FlattenRecord(); flattenRecord.setCompositeRecord(compositeRecord); flattenRecord.setId("id456"); - + flattenedMappedTable.putItem(r -> r.item(flattenRecord)); - + NestedRecordWithUpdateBehavior updateNestedRecord = new NestedRecordWithUpdateBehavior(); updateNestedRecord.setNestedCounter(100L); - + CompositeRecord updateCompositeRecord = new CompositeRecord(); updateCompositeRecord.setNestedRecord(updateNestedRecord); - + FlattenRecord updatedFlattenRecord = new FlattenRecord(); updatedFlattenRecord.setId("id456"); updatedFlattenRecord.setCompositeRecord(updateCompositeRecord); @@ -697,7 +700,7 @@ 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); @@ -710,27 +713,27 @@ public void when_updatingNestedObjectWithMultipleLevelFlattened_existingInformat FlattenRecord flattenRecord = new FlattenRecord(); flattenRecord.setCompositeRecord(compositeRecord); flattenRecord.setId("id789"); - + flattenedMappedTable.putItem(r -> r.item(flattenRecord)); - + NestedRecordWithUpdateBehavior updateOuterNestedRecord = new NestedRecordWithUpdateBehavior(); updateOuterNestedRecord.setNestedCounter(100L); - + NestedRecordWithUpdateBehavior updateInnerNestedRecord = new NestedRecordWithUpdateBehavior(); updateInnerNestedRecord.setNestedCounter(50L); - + updateOuterNestedRecord.setNestedRecord(updateInnerNestedRecord); - + CompositeRecord updateCompositeRecord = new CompositeRecord(); updateCompositeRecord.setNestedRecord(updateOuterNestedRecord); - + FlattenRecord updateFlattenRecord = new FlattenRecord(); updateFlattenRecord.setCompositeRecord(updateCompositeRecord); updateFlattenRecord.setId("id789"); - + FlattenRecord persistedFlattenedRecord = flattenedMappedTable.updateItem(r -> r.item(updateFlattenRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - + assertThat(persistedFlattenedRecord.getCompositeRecord()).isNotNull(); verifyMultipleLevelNestingTargetedUpdateBehavior(persistedFlattenedRecord.getCompositeRecord().getNestedRecord(), 100L, 50L, TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1); @@ -771,7 +774,7 @@ public void updateBehaviour_onItemWithRootAttributeNameContainingReservedMarker_ thrown.expectMessage("Attribute name 'attr_NESTED_ATTR_UPDATE_' contains reserved marker " + "'_NESTED_ATTR_UPDATE_' and is not allowed."); - BeanWithInvalidAttributeNameOnRoot record = new BeanWithInvalidAttributeNameOnRoot(); + InvalidRootAttributeBean record = new InvalidRootAttributeBean(); record.setId("1"); record.setAttr_NESTED_ATTR_UPDATE_(INSTANT_1); @@ -786,10 +789,10 @@ public void updateBehaviour_onItemWithNestedAttributeNameContainingReservedMarke thrown.expectMessage("Attribute name 'attr_NESTED_ATTR_UPDATE_' contains reserved marker " + "'_NESTED_ATTR_UPDATE_' and is not allowed."); - BeanWithInvalidAttributeNameOnNestedLevel record = new BeanWithInvalidAttributeNameOnNestedLevel(); + InvalidNestedAttributeBean record = new InvalidNestedAttributeBean(); record.setId("1"); - BeanChildWithInvalidAttributeNameOnNestedLevel childBean = new BeanChildWithInvalidAttributeNameOnNestedLevel(); + InvalidNestedAttributeChild childBean = new InvalidNestedAttributeChild(); childBean.setId("2"); childBean.setAttr_NESTED_ATTR_UPDATE_(INSTANT_1); record.setNestedChildAttribute(childBean); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnNestedLevel.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeBean.java similarity index 65% rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnNestedLevel.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeBean.java index 827151c5ec68..5e3da3dd1959 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnNestedLevel.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeBean.java @@ -23,37 +23,37 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; @DynamoDbBean -public class BeanWithInvalidAttributeNameOnNestedLevel { +public class InvalidNestedAttributeBean { private String id; - private BeanChildWithInvalidAttributeNameOnNestedLevel nestedChildAttribute; + private InvalidNestedAttributeChild nestedChildAttribute; @DynamoDbPartitionKey public String getId() { return id; } - public BeanWithInvalidAttributeNameOnNestedLevel setId(String id) { + public InvalidNestedAttributeBean setId(String id) { this.id = id; return this; } - public BeanChildWithInvalidAttributeNameOnNestedLevel getNestedChildAttribute() { + public InvalidNestedAttributeChild getNestedChildAttribute() { return nestedChildAttribute; } - public BeanWithInvalidAttributeNameOnNestedLevel setNestedChildAttribute( - BeanChildWithInvalidAttributeNameOnNestedLevel nestedChildAttribute) { + public InvalidNestedAttributeBean setNestedChildAttribute( + InvalidNestedAttributeChild nestedChildAttribute) { this.nestedChildAttribute = nestedChildAttribute; return this; } @DynamoDbBean - public static class BeanChildWithInvalidAttributeNameOnNestedLevel { + public static class InvalidNestedAttributeChild { private String id; - private BeanChildWithInvalidAttributeNameOnNestedLevel nestedChildAttribute; + private InvalidNestedAttributeChild nestedChildAttribute; private Instant childAttr_NESTED_ATTR_UPDATE_; @DynamoDbPartitionKey @@ -61,17 +61,17 @@ public String getId() { return id; } - public BeanChildWithInvalidAttributeNameOnNestedLevel setId(String id) { + public InvalidNestedAttributeChild setId(String id) { this.id = id; return this; } - public BeanChildWithInvalidAttributeNameOnNestedLevel getNestedChildAttribute() { + public InvalidNestedAttributeChild getNestedChildAttribute() { return nestedChildAttribute; } - public BeanChildWithInvalidAttributeNameOnNestedLevel setNestedChildAttribute( - BeanChildWithInvalidAttributeNameOnNestedLevel nestedChildAttribute) { + public InvalidNestedAttributeChild setNestedChildAttribute( + InvalidNestedAttributeChild nestedChildAttribute) { this.nestedChildAttribute = nestedChildAttribute; return this; } @@ -81,9 +81,9 @@ public Instant getAttr_NESTED_ATTR_UPDATE_() { return childAttr_NESTED_ATTR_UPDATE_; } - public BeanChildWithInvalidAttributeNameOnNestedLevel setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + public InvalidNestedAttributeChild setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { this.childAttr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; return this; } } -} +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnNestedLevel.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeRecord.java similarity index 58% rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnNestedLevel.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeRecord.java index 8232f802a531..7d2bac5a8a9f 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnNestedLevel.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeRecord.java @@ -17,52 +17,52 @@ import java.time.Instant; -public class RecordWithInvalidAttributeNameOnNestedLevel { +public class InvalidNestedAttributeRecord { private String id; - private RecordWithReservedMarkerNestedChildAttribute nestedChildAttribute; + private InvalidNestedAttributeRecordChild nestedChildAttribute; public String getId() { return id; } - public RecordWithInvalidAttributeNameOnNestedLevel setId(String id) { + public InvalidNestedAttributeRecord setId(String id) { this.id = id; return this; } - public RecordWithReservedMarkerNestedChildAttribute getNestedChildAttribute() { + public InvalidNestedAttributeRecordChild getNestedChildAttribute() { return nestedChildAttribute; } - public RecordWithInvalidAttributeNameOnNestedLevel setNestedChildAttribute( - RecordWithReservedMarkerNestedChildAttribute nestedChildAttribute) { + public InvalidNestedAttributeRecord setNestedChildAttribute( + InvalidNestedAttributeRecordChild nestedChildAttribute) { this.nestedChildAttribute = nestedChildAttribute; return this; } - public static class RecordWithReservedMarkerNestedChildAttribute { + public static class InvalidNestedAttributeRecordChild { private String id; - private RecordWithReservedMarkerNestedChildAttribute nestedChildAttribute; + private InvalidNestedAttributeRecordChild nestedChildAttribute; private Instant childAttr_NESTED_ATTR_UPDATE_; public String getId() { return id; } - public RecordWithReservedMarkerNestedChildAttribute setId(String id) { + public InvalidNestedAttributeRecordChild setId(String id) { this.id = id; return this; } - public RecordWithReservedMarkerNestedChildAttribute getNestedChildAttribute() { + public InvalidNestedAttributeRecordChild getNestedChildAttribute() { return nestedChildAttribute; } - public RecordWithReservedMarkerNestedChildAttribute setNestedChildAttribute( - RecordWithReservedMarkerNestedChildAttribute nestedChildAttribute) { + public InvalidNestedAttributeRecordChild setNestedChildAttribute( + InvalidNestedAttributeRecordChild nestedChildAttribute) { this.nestedChildAttribute = nestedChildAttribute; return this; } @@ -71,9 +71,9 @@ public Instant getAttr_NESTED_ATTR_UPDATE_() { return childAttr_NESTED_ATTR_UPDATE_; } - public RecordWithReservedMarkerNestedChildAttribute setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + public InvalidNestedAttributeRecordChild setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { this.childAttr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; return this; } } -} +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnRoot.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeBean.java similarity index 86% rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnRoot.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeBean.java index 14f4f4c46baf..3efd7597ff70 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/BeanWithInvalidAttributeNameOnRoot.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeBean.java @@ -23,7 +23,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; @DynamoDbBean -public class BeanWithInvalidAttributeNameOnRoot { +public class InvalidRootAttributeBean { private String id; private Instant attr_NESTED_ATTR_UPDATE_; @@ -33,7 +33,7 @@ public String getId() { return id; } - public BeanWithInvalidAttributeNameOnRoot setId(String id) { + public InvalidRootAttributeBean setId(String id) { this.id = id; return this; } @@ -43,8 +43,8 @@ public Instant getAttr_NESTED_ATTR_UPDATE_() { return attr_NESTED_ATTR_UPDATE_; } - public BeanWithInvalidAttributeNameOnRoot setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + public InvalidRootAttributeBean setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { this.attr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; return this; } -} +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnRoot.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeRecord.java similarity index 81% rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnRoot.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeRecord.java index faf5d8fa8550..813641ee4d3d 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithInvalidAttributeNameOnRoot.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeRecord.java @@ -17,7 +17,7 @@ import java.time.Instant; -public class RecordWithInvalidAttributeNameOnRoot { +public class InvalidRootAttributeRecord { private String id; private Instant attr_NESTED_ATTR_UPDATE_; @@ -26,7 +26,7 @@ public String getId() { return id; } - public RecordWithInvalidAttributeNameOnRoot setId(String id) { + public InvalidRootAttributeRecord setId(String id) { this.id = id; return this; } @@ -35,8 +35,8 @@ public Instant getAttr_NESTED_ATTR_UPDATE_() { return attr_NESTED_ATTR_UPDATE_; } - public RecordWithInvalidAttributeNameOnRoot setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + public InvalidRootAttributeRecord setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { this.attr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; return this; } -} +} \ No newline at end of file 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 df2e92c57392..a2bcdc4dd690 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 @@ -36,7 +36,7 @@ public class NestedRecordWithUpdateBehavior { private Long nestedCounter; private NestedRecordWithUpdateBehavior nestedRecord; private String attribute; - private List nestedRecordList; + private List nestedRecordList; @DynamoDbPartitionKey public String getId() { @@ -109,9 +109,9 @@ public void setAttribute(String attribute) { this.attribute = attribute; } - public List getNestedRecordList() { return nestedRecordList;} + public List getNestedRecordList() { return nestedRecordList;} - public void setNestedRecordList(List 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 ad396ed28d00..202f7ee13ac7 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 @@ -41,7 +41,7 @@ public class RecordWithUpdateBehaviors { private Instant formattedLastAutoUpdatedOn; private NestedRecordWithUpdateBehavior nestedRecord; private String key; - private List nestedRecordList; + private List nestedRecordList; @DynamoDbPartitionKey public String getId() { @@ -136,9 +136,9 @@ public void setNestedRecord(NestedRecordWithUpdateBehavior nestedRecord) { this.nestedRecord = nestedRecord; } - public List getNestedRecordList() { return nestedRecordList;} + public List getNestedRecordList() { return nestedRecordList;} - public void setNestedRecordList(List 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/NestedRecordListElement.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/TimestampListElement.java similarity index 97% rename from services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordListElement.java rename to services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/TimestampListElement.java index 6cf9450f349c..23e7abe9d94c 100644 --- 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/TimestampListElement.java @@ -21,7 +21,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @DynamoDbBean -public class NestedRecordListElement { +public class TimestampListElement { private String id; private String attribute; private Instant timeAttributeElement; @@ -51,4 +51,4 @@ public Instant getTimeAttributeElement() { public void setTimeAttributeElement(Instant timeAttributeElement) { this.timeAttributeElement = timeAttributeElement; } -} +} \ No newline at end of file From e829dcbd2f22b97c38d6917d36b08a6a6454b85e Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 4 Feb 2026 08:30:03 +0200 Subject: [PATCH 11/18] Exclude nested update behavior functionality - Remove NestedUpdateBehaviorTest.java - tests for nested update behavior support - Remove UpdateBehaviorTestModels.java - test models for nested update behavior These files will be included in a separate PR for nested update behavior support. --- .../NestedUpdateBehaviorTest.java | 529 ------------- .../models/UpdateBehaviorTestModels.java | 715 ------------------ 2 files changed, 1244 deletions(-) delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UpdateBehaviorTestModels.java diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java deleted file mode 100644 index d8d2989702d3..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/NestedUpdateBehaviorTest.java +++ /dev/null @@ -1,529 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.functionaltests; - -import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.NestedStaticChildRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.buildStaticImmutableSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.buildStaticImmutableSchemaForSimpleRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.buildStaticSchemaForNestedRecordWithList; -import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.buildStaticSchemaForSimpleRecordWithList; - -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneOffset; -import java.util.Arrays; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mockito; -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.AutoGeneratedTimestampRecordExtension; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.NestedBeanWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.NestedImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.NestedStaticRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.SimpleBeanWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.SimpleImmutableRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.UpdateBehaviorTestModels.SimpleStaticRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; -import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; -import software.amazon.awssdk.enhanced.dynamodb.model.IgnoreNullsMode; -import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; - -public class NestedUpdateBehaviorTest extends LocalDynamoDbSyncTestBase { - - private static final String TIMESTAMP_STRING = "2019-01-13T14:00:00Z"; - private static final String BASE_TABLE_NAME = "update-behavior-test"; - private static final Clock mockClock = Mockito.mock(Clock.class); - private static final Instant MOCKED_INSTANT_NOW = Instant.now(Clock.fixed(Instant.parse(TIMESTAMP_STRING), ZoneOffset.UTC)); - - private DynamoDbEnhancedClient enhancedClient; - private String currentTestTableName; - - @Before - public void beforeClass() { - Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_NOW); - enhancedClient = DynamoDbEnhancedClient.builder() - .dynamoDbClient(getDynamoDbClient()) - .extensions(AutoGeneratedTimestampRecordExtension.builder() - .baseClock(mockClock) - .build()) - .build(); - } - - @After - public void deleteTable() { - try { - if (currentTestTableName != null) { - getDynamoDbClient().deleteTable(r -> r.tableName(currentTestTableName)); - } - } catch (ResourceNotFoundException e) { - // Table didn't get created, ignore. - } - } - - @Test - public void beanSchema_simpleRecord_writeIfNotExists_isRespected() { - TableSchema schema = BeanTableSchema.create(SimpleBeanWithList.class); - DynamoDbTable table = - createAndPut("simple-bean-write-if-not-exists", - schema, - new SimpleBeanWithList() - .setId("1") - .setAttr("attr_level1") - .setChildList(Arrays.asList( - new UpdateBehaviorTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), - new UpdateBehaviorTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2")))); - - SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr()).isEqualTo("attr_level1"); - assertThat(result.getChildList()).hasSize(2); - - // update with new attr value - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) - result.setAttr("updated_level1"); - table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - - SimpleBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(updated.getAttr()).isEqualTo("attr_level1"); // should NOT change - - table.deleteTable(); - } - - @Test - public void beanSchema_nestedRecord_writeIfNotExists_isRespected() { - TableSchema schema = BeanTableSchema.create(NestedBeanWithList.class); - DynamoDbTable table = - createAndPut("nested-bean-write-if-not-exists", - schema, - new NestedBeanWithList() - .setId("1") - .setAttr("attr_level1") - .setLevel2(new UpdateBehaviorTestModels.NestedBeanChild().setAttr("attr_level2"))); - - NestedBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr()).isEqualTo("attr_level1"); - assertThat(result.getLevel2()).isNotNull(); - assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); - - // update with new attr values - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) - result.setAttr("updated_level1"); - result.getLevel2().setAttr("updated_level2"); - table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - - NestedBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(updated.getAttr()).isEqualTo("attr_level1"); // should NOT change - assertThat(updated.getLevel2().getAttr()).isEqualTo("attr_level2"); // should NOT change - - table.deleteTable(); - } - - @Test - public void immutableSchema_simpleRecord_writeIfNotExists_isRespected() { - TableSchema schema = ImmutableTableSchema.create(SimpleImmutableRecordWithList.class); - DynamoDbTable table = - createAndPut("simple-immutable-write-if-not-exists", - schema, - SimpleImmutableRecordWithList - .builder() - .id("1") - .attr("attr_level1") - .childList(Arrays.asList( - UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) - .build()); - - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr()).isEqualTo("attr_level1"); - assertThat(result.getChildList()).hasSize(2); - - // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) - SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() - .id("1").attr("updated_level1") - .build(); - table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - - SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(afterUpdate.getAttr()).isEqualTo("attr_level1"); // should NOT change - - table.deleteTable(); - } - - @Test - public void immutableSchema_nestedRecord_writeIfNotExists_isRespected() { - TableSchema schema = ImmutableTableSchema.create(NestedImmutableRecordWithList.class); - DynamoDbTable table = - createAndPut("nested-immutable-write-if-not-exists", - schema, - NestedImmutableRecordWithList - .builder() - .id("1") - .attr("attr_level1") - .level2(UpdateBehaviorTestModels.NestedImmutableChildRecordWithList - .builder().attr("attr_level2").build()) - .build()); - - NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr()).isEqualTo("attr_level1"); - assertThat(result.getLevel2()).isNotNull(); - assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); - - // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) - NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() - .id("1").attr("updated_level1") - .build(); - table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - - NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(afterUpdate.getAttr()).isEqualTo("attr_level1"); // should NOT change - - table.deleteTable(); - } - - @Test - public void staticSchema_simpleRecord_writeIfNotExists_isRespected() { - DynamoDbTable table = - createAndPut("simple-static-write-if-not-exists", - buildStaticSchemaForSimpleRecordWithList(), - new SimpleStaticRecordWithList() - .setId("1") - .setAttr("attr_level1")); - - SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr()).isEqualTo("attr_level1"); - - // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) - result.setAttr("updated_level1"); - table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - - SimpleStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(updated.getAttr()).isEqualTo("attr_level1"); // should NOT change - - table.deleteTable(); - } - - @Test - public void staticSchema_nestedRecord_writeIfNotExists_isRespected() { - DynamoDbTable table = - createAndPut( - "nested-static-write-if-not-exists", - buildStaticSchemaForNestedRecordWithList(), - new NestedStaticRecordWithList() - .setId("1") - .setAttr("attr_level1") - .setLevel2(new NestedStaticChildRecordWithList().setAttr("attr_level2"))); - - NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr()).isEqualTo("attr_level1"); - assertThat(result.getLevel2()).isNotNull(); - assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); - - // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) - result.setAttr("updated_level1"); - result.getLevel2().setAttr("updated_level2"); - table.updateItem(r -> r.item(result).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - - NestedStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(updated.getAttr()).isEqualTo("attr_level1"); // should NOT change - assertThat(updated.getLevel2().getAttr()).isEqualTo("attr_level2"); // should NOT change - - table.deleteTable(); - } - - @Test - public void staticImmutableSchema_simpleRecord_writeIfNotExists_isRespected() { - DynamoDbTable table = - createAndPut("simple-static-immutable-write-if-not-exists", - buildStaticImmutableSchemaForSimpleRecordWithList(), - SimpleImmutableRecordWithList - .builder() - .id("1") - .attr("attr_level1") - .childList(Arrays.asList( - UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) - .build()); - - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr()).isEqualTo("attr_level1"); - - // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) - SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() - .id("1").attr("updated_level1") - .build(); - table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - - SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(afterUpdate.getAttr()).isEqualTo("attr_level1"); // should NOT change - - table.deleteTable(); - } - - @Test - public void staticImmutableSchema_nestedRecord_writeIfNotExists_isRespected() { - DynamoDbTable table = - createAndPut("nested-static-immutable-write-if-not-exists", - buildStaticImmutableSchemaForNestedRecordWithList(), - NestedImmutableRecordWithList - .builder() - .id("1") - .attr("attr_level1") - .level2(UpdateBehaviorTestModels.NestedImmutableChildRecordWithList - .builder().attr("attr_level2").build()) - .build()); - - NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getAttr()).isEqualTo("attr_level1"); - assertThat(result.getLevel2()).isNotNull(); - assertThat(result.getLevel2().getAttr()).isEqualTo("attr_level2"); - - // update with new attr - should NOT change due to WRITE_IF_NOT_EXISTS (requires SCALAR_ONLY mode) - NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() - .id("1").attr("updated_level1") - .build(); - table.updateItem(r -> r.item(updated).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - - NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(afterUpdate.getAttr()).isEqualTo("attr_level1"); // should NOT change - - table.deleteTable(); - } - - @Test - public void beanSchema_simpleRecord_writeAlways_updatesValues() { - TableSchema schema = BeanTableSchema.create(SimpleBeanWithList.class); - SimpleBeanWithList initial = new SimpleBeanWithList() - .setId("1") - .setAttr("attr_level1") - .setChildList(Arrays.asList( - new UpdateBehaviorTestModels.SimpleBeanChild().setId("child1").setAttr("attr_child1"), - new UpdateBehaviorTestModels.SimpleBeanChild().setId("child2").setAttr("attr_child2"))); - initial.setId("initial_id"); - DynamoDbTable table = createAndPut("simple-bean-write-always", schema, initial); - - SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("initial_id"))); - assertThat(result.getId()).isEqualTo("initial_id"); - - // update id (no annotation, defaults to WRITE_ALWAYS) - should change - result.setId("updated_id"); - table.updateItem(result); - - SimpleBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); - assertThat(updated).isNotNull(); - assertThat(updated.getId()).isEqualTo("updated_id"); - - table.deleteTable(); - } - - @Test - public void beanSchema_nestedRecord_writeAlways_updatesValues() { - TableSchema schema = BeanTableSchema.create(NestedBeanWithList.class); - NestedBeanWithList initial = new NestedBeanWithList() - .setId("1") - .setAttr("attr_level1") - .setLevel2(new UpdateBehaviorTestModels.NestedBeanChild().setAttr("attr_level2")); - initial.setId("initial_id"); - DynamoDbTable table = createAndPut("nested-bean-write-always", schema, initial); - - NestedBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("initial_id"))); - assertThat(result.getId()).isEqualTo("initial_id"); - - // update id - should change (WRITE_ALWAYS is default) - result.setId("updated_id"); - table.updateItem(result); - - NestedBeanWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); - assertThat(updated).isNotNull(); - assertThat(updated.getId()).isEqualTo("updated_id"); - - table.deleteTable(); - } - - @Test - public void immutableSchema_simpleRecord_writeAlways_updatesValues() { - TableSchema schema = ImmutableTableSchema.create(SimpleImmutableRecordWithList.class); - SimpleImmutableRecordWithList initial = - SimpleImmutableRecordWithList - .builder() - .id("1") - .attr("attr_level1") - .childList(Arrays.asList( - UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) - .build(); - - DynamoDbTable table = createAndPut("simple-immutable-write-always", schema, initial); - - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getId()).isEqualTo("1"); - - // update with new id - should change (WRITE_ALWAYS is default) - SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() - .id("updated_id").attr("attr_level1") - .build(); - table.updateItem(updated); - - SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); - assertThat(afterUpdate).isNotNull(); - assertThat(afterUpdate.getId()).isEqualTo("updated_id"); - - table.deleteTable(); - } - - @Test - public void immutableSchema_nestedRecord_writeAlways_updatesValues() { - TableSchema schema = ImmutableTableSchema.create(NestedImmutableRecordWithList.class); - NestedImmutableRecordWithList initial = - NestedImmutableRecordWithList - .builder() - .id("1") - .attr("attr_level1") - .level2(UpdateBehaviorTestModels.NestedImmutableChildRecordWithList - .builder().attr("attr_level2").build()) - .build(); - DynamoDbTable table = createAndPut("nested-immutable-write-always", schema, initial); - - NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getId()).isEqualTo("1"); - - // update with new id - should change (WRITE_ALWAYS is default) - NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() - .id("updated_id").attr("attr_level1") - .build(); - table.updateItem(updated); - - NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); - assertThat(afterUpdate).isNotNull(); - assertThat(afterUpdate.getId()).isEqualTo("updated_id"); - - table.deleteTable(); - } - - @Test - public void staticSchema_simpleRecord_writeAlways_updatesValues() { - DynamoDbTable table = - createAndPut("simple-static-write-always", - buildStaticSchemaForSimpleRecordWithList(), - new SimpleStaticRecordWithList() - .setId("1") - .setAttr("attr_level1")); - - SimpleStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getId()).isEqualTo("1"); - - // update id - should change (WRITE_ALWAYS is default) - result.setId("updated_id"); - table.updateItem(result); - - SimpleStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); - assertThat(updated).isNotNull(); - assertThat(updated.getId()).isEqualTo("updated_id"); - - table.deleteTable(); - } - - @Test - public void staticSchema_nestedRecord_writeAlways_updatesValues() { - DynamoDbTable table = createAndPut( - "nested-static-write-always", - buildStaticSchemaForNestedRecordWithList(), - new NestedStaticRecordWithList() - .setId("1") - .setAttr("attr_level1") - .setLevel2(new UpdateBehaviorTestModels.NestedStaticChildRecordWithList().setAttr("attr_level2"))); - - NestedStaticRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getId()).isEqualTo("1"); - - // update id - should change (WRITE_ALWAYS is default) - result.setId("updated_id"); - table.updateItem(result); - - NestedStaticRecordWithList updated = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); - assertThat(updated).isNotNull(); - assertThat(updated.getId()).isEqualTo("updated_id"); - - table.deleteTable(); - } - - @Test - public void staticImmutableSchema_simpleRecord_writeAlways_updatesValues() { - DynamoDbTable table = - createAndPut("simple-static-immutable-write-always", - buildStaticImmutableSchemaForSimpleRecordWithList(), - SimpleImmutableRecordWithList - .builder() - .id("1") - .attr("attr_level1") - .childList(Arrays.asList( - UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child1").attr("attr_child1").build(), - UpdateBehaviorTestModels.SimpleImmutableChild.builder().id("child2").attr("attr_child2").build())) - .build()); - - SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getId()).isEqualTo("1"); - - // update with new id - should change (WRITE_ALWAYS is default) - SimpleImmutableRecordWithList updated = SimpleImmutableRecordWithList.builder() - .id("updated_id").attr("attr_level1") - .build(); - table.updateItem(updated); - - SimpleImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); - assertThat(afterUpdate).isNotNull(); - assertThat(afterUpdate.getId()).isEqualTo("updated_id"); - - table.deleteTable(); - } - - @Test - public void staticImmutableSchema_nestedRecord_writeAlways_updatesValues() { - DynamoDbTable table = - createAndPut("nested-static-immutable-write-always", - buildStaticImmutableSchemaForNestedRecordWithList(), - NestedImmutableRecordWithList.builder() - .id("1") - .attr("attr_level1") - .level2(UpdateBehaviorTestModels.NestedImmutableChildRecordWithList - .builder().attr("attr_level2").build()) - .build()); - - NestedImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - assertThat(result.getId()).isEqualTo("1"); - - // update with new id - should change (WRITE_ALWAYS is default) - NestedImmutableRecordWithList updated = NestedImmutableRecordWithList.builder() - .id("updated_id").attr("attr_level1") - .build(); - table.updateItem(updated); - - NestedImmutableRecordWithList afterUpdate = table.getItem(r -> r.key(k -> k.partitionValue("updated_id"))); - assertThat(afterUpdate).isNotNull(); - assertThat(afterUpdate.getId()).isEqualTo("updated_id"); - - table.deleteTable(); - } - - private DynamoDbTable createAndPut(String tableSuffix, TableSchema schema, T item) { - currentTestTableName = BASE_TABLE_NAME + tableSuffix + System.nanoTime(); - DynamoDbTable table = enhancedClient.table(currentTestTableName, schema); - table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - table.putItem(item); - return table; - } -} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UpdateBehaviorTestModels.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UpdateBehaviorTestModels.java deleted file mode 100644 index 71f12eef97b3..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/UpdateBehaviorTestModels.java +++ /dev/null @@ -1,715 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; - -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.updateBehavior; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; -import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; -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.DynamoDbImmutable; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; -import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; - -/** - * Test models specifically designed for update behavior functionality testing. These models focus on the "attr" attribute - * annotated with @DynamoDbUpdateBehavior annotation and are used by NestedUpdateBehaviorTest. - */ -public final class UpdateBehaviorTestModels { - - private UpdateBehaviorTestModels() { - } - - @DynamoDbBean - public static class SimpleBeanWithList { - private String id; - private String attr; - private List childList; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public SimpleBeanWithList setId(String id) { - this.id = id; - return this; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public SimpleBeanWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - public List getChildList() { - return childList; - } - - public SimpleBeanWithList setChildList(List childList) { - this.childList = childList; - return this; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof SimpleBeanWithList)) { - return false; - } - - SimpleBeanWithList that = (SimpleBeanWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) && Objects.equals(childList, that.childList); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(id); - result = 31 * result + Objects.hashCode(attr); - result = 31 * result + Objects.hashCode(childList); - return result; - } - } - - @DynamoDbBean - public static class SimpleBeanChild { - private String id; - private String attr; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public SimpleBeanChild setId(String id) { - this.id = id; - return this; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public SimpleBeanChild setAttr(String attr) { - this.attr = attr; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleBeanChild that = (SimpleBeanChild) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr); - } - - @Override - public int hashCode() { - return Objects.hash(id, attr); - } - } - - @DynamoDbBean - public static class NestedBeanWithList { - private String id; - private String attr; - private NestedBeanChild level2; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public NestedBeanWithList setId(String id) { - this.id = id; - return this; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public NestedBeanWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - public NestedBeanChild getLevel2() { - return level2; - } - - public NestedBeanWithList setLevel2(NestedBeanChild level2) { - this.level2 = level2; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedBeanWithList that = (NestedBeanWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) - && Objects.equals(level2, that.level2); - } - - @Override - public int hashCode() { - return Objects.hash(id, attr, level2); - } - } - - @DynamoDbBean - public static class NestedBeanChild { - private String attr; - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public NestedBeanChild setAttr(String attr) { - this.attr = attr; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedBeanChild that = (NestedBeanChild) o; - return Objects.equals(attr, that.attr); - } - - @Override - public int hashCode() { - return Objects.hash(attr - ); - } - } - - @DynamoDbImmutable(builder = SimpleImmutableRecordWithList.Builder.class) - public static final class SimpleImmutableRecordWithList { - private final String id; - private final String attr; - private final List childList; - - private SimpleImmutableRecordWithList(Builder b) { - this.id = b.id; - this.attr = b.attr; - this.childList = b.childList; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public List getChildList() { - return childList == null ? null : Collections.unmodifiableList(childList); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private List childList; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder childList(List childList) { - this.childList = childList; - return this; - } - - public SimpleImmutableRecordWithList build() { - return new SimpleImmutableRecordWithList(this); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleImmutableRecordWithList that = (SimpleImmutableRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) - && Objects.equals(childList, that.childList); - } - - @Override - public int hashCode() { - return Objects.hash(id, attr, childList); - } - } - - @DynamoDbImmutable(builder = SimpleImmutableChild.Builder.class) - public static final class SimpleImmutableChild { - private final String id; - private final String attr; - - private SimpleImmutableChild(Builder b) { - this.id = b.id; - this.attr = b.attr; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public SimpleImmutableChild build() { - return new SimpleImmutableChild(this); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleImmutableChild that = (SimpleImmutableChild) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr); - } - - @Override - public int hashCode() { - return Objects.hash(id, attr); - } - } - - @DynamoDbImmutable(builder = NestedImmutableRecordWithList.Builder.class) - public static final class NestedImmutableRecordWithList { - private final String id; - private final String attr; - private final NestedImmutableChildRecordWithList level2; - - private NestedImmutableRecordWithList(Builder b) { - this.id = b.id; - this.attr = b.attr; - - this.level2 = b.level2; - } - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public NestedImmutableChildRecordWithList getLevel2() { - return level2; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String id; - private String attr; - private NestedImmutableChildRecordWithList level2; - - public Builder id(String id) { - this.id = id; - return this; - } - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public Builder level2(NestedImmutableChildRecordWithList level2) { - this.level2 = level2; - return this; - } - - public NestedImmutableRecordWithList build() { - return new NestedImmutableRecordWithList(this); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedImmutableRecordWithList that = (NestedImmutableRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) - && Objects.equals(level2, that.level2); - } - - @Override - public int hashCode() { - return Objects.hash(id, attr, level2); - } - } - - @DynamoDbImmutable(builder = NestedImmutableChildRecordWithList.Builder.class) - public static final class NestedImmutableChildRecordWithList { - private final String attr; - - private NestedImmutableChildRecordWithList(Builder b) { - this.attr = b.attr; - - } - - @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) - public String getAttr() { - return attr; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String attr; - - public Builder attr(String attr) { - this.attr = attr; - return this; - } - - public NestedImmutableChildRecordWithList build() { - return new NestedImmutableChildRecordWithList(this); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedImmutableChildRecordWithList that = (NestedImmutableChildRecordWithList) o; - return Objects.equals(attr, that.attr); - } - - @Override - public int hashCode() { - return Objects.hash(attr); - } - } - - public static class SimpleStaticRecordWithList { - private String id; - private String attr; - - public String getId() { - return id; - } - - public SimpleStaticRecordWithList setId(String id) { - this.id = id; - return this; - } - - public String getAttr() { - return attr; - } - - public SimpleStaticRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleStaticRecordWithList that = (SimpleStaticRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr); - } - - @Override - public int hashCode() { - return Objects.hash(id, attr); - } - } - - public static class NestedStaticRecordWithList { - private String id; - private String attr; - private NestedStaticChildRecordWithList level2; - - public String getId() { - return id; - } - - public NestedStaticRecordWithList setId(String id) { - this.id = id; - return this; - } - - public String getAttr() { - return attr; - } - - public NestedStaticRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - public NestedStaticChildRecordWithList getLevel2() { - return level2; - } - - public NestedStaticRecordWithList setLevel2(NestedStaticChildRecordWithList level2) { - this.level2 = level2; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedStaticRecordWithList that = (NestedStaticRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(attr, that.attr) - && Objects.equals(level2, that.level2); - } - - @Override - public int hashCode() { - return Objects.hash(id, attr, level2); - } - } - - public static class NestedStaticChildRecordWithList { - private String attr; - - public String getAttr() { - return attr; - } - - public NestedStaticChildRecordWithList setAttr(String attr) { - this.attr = attr; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedStaticChildRecordWithList that = (NestedStaticChildRecordWithList) o; - return Objects.equals(attr, that.attr); - } - - @Override - public int hashCode() { - return Objects.hash(attr); - } - } - - public static TableSchema buildStaticSchemaForSimpleRecordWithList() { - return StaticTableSchema.builder(SimpleStaticRecordWithList.class) - .newItemSupplier(SimpleStaticRecordWithList::new) - .addAttribute(String.class, a -> a.name("id") - .getter(SimpleStaticRecordWithList::getId) - .setter(SimpleStaticRecordWithList::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(SimpleStaticRecordWithList::getAttr) - .setter(SimpleStaticRecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .build(); - } - - public static TableSchema buildStaticSchemaForNestedRecordWithList() { - TableSchema level2Schema = - StaticTableSchema.builder(NestedStaticChildRecordWithList.class) - .newItemSupplier(NestedStaticChildRecordWithList::new) - .addAttribute(String.class, a -> a.name("attr") - .getter(NestedStaticChildRecordWithList::getAttr) - .setter(NestedStaticChildRecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .build(); - - return StaticTableSchema.builder(NestedStaticRecordWithList.class) - .newItemSupplier(NestedStaticRecordWithList::new) - .addAttribute(String.class, a -> a.name("id") - .getter(NestedStaticRecordWithList::getId) - .setter(NestedStaticRecordWithList::setId) - .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(NestedStaticRecordWithList::getAttr) - .setter(NestedStaticRecordWithList::setAttr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(EnhancedType.documentOf(NestedStaticChildRecordWithList.class, level2Schema), - a -> a.name("level2") - .getter(NestedStaticRecordWithList::getLevel2) - .setter(NestedStaticRecordWithList::setLevel2)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForSimpleRecordWithList() { - TableSchema childSchema = - StaticImmutableTableSchema.builder(SimpleImmutableChild.class, - SimpleImmutableChild.Builder.class) - .newItemBuilder(SimpleImmutableChild::builder, - SimpleImmutableChild.Builder::build) - .addAttribute(String.class, a -> a.name("id") - .getter(SimpleImmutableChild::getId) - .setter(SimpleImmutableChild.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(SimpleImmutableChild::getAttr) - .setter(SimpleImmutableChild.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .build(); - - return StaticImmutableTableSchema.builder(SimpleImmutableRecordWithList.class, - SimpleImmutableRecordWithList.Builder.class) - .newItemBuilder(SimpleImmutableRecordWithList::builder, - SimpleImmutableRecordWithList.Builder::build) - .addAttribute(String.class, a -> a.name("id") - .getter(SimpleImmutableRecordWithList::getId) - .setter(SimpleImmutableRecordWithList.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(SimpleImmutableRecordWithList::getAttr) - .setter(SimpleImmutableRecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(EnhancedType.listOf(EnhancedType.documentOf(SimpleImmutableChild.class, - childSchema)), - a -> a.name("childList") - .getter(SimpleImmutableRecordWithList::getChildList) - .setter(SimpleImmutableRecordWithList.Builder::childList)) - .build(); - } - - public static TableSchema buildStaticImmutableSchemaForNestedRecordWithList() { - TableSchema level2Schema = - StaticImmutableTableSchema.builder(NestedImmutableChildRecordWithList.class, - NestedImmutableChildRecordWithList.Builder.class) - .newItemBuilder(NestedImmutableChildRecordWithList::builder, - NestedImmutableChildRecordWithList.Builder::build) - .addAttribute(String.class, a -> a.name("attr") - .getter(NestedImmutableChildRecordWithList::getAttr) - .setter(NestedImmutableChildRecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .build(); - - return StaticImmutableTableSchema.builder(NestedImmutableRecordWithList.class, - NestedImmutableRecordWithList.Builder.class) - .newItemBuilder(NestedImmutableRecordWithList::builder, - NestedImmutableRecordWithList.Builder::build) - .addAttribute(String.class, a -> a.name("id") - .getter(NestedImmutableRecordWithList::getId) - .setter(NestedImmutableRecordWithList.Builder::id) - .tags(primaryPartitionKey())) - .addAttribute(String.class, a -> a.name("attr") - .getter(NestedImmutableRecordWithList::getAttr) - .setter(NestedImmutableRecordWithList.Builder::attr) - .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) - .addAttribute(EnhancedType.documentOf(NestedImmutableChildRecordWithList.class, - level2Schema), - a -> a.name("level2") - .getter(NestedImmutableRecordWithList::getLevel2) - .setter(NestedImmutableRecordWithList.Builder::level2)) - .build(); - } -} \ No newline at end of file From ab4c6834e0a420b201a607c124804fa8dbcad3bc Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 4 Feb 2026 10:35:21 +0200 Subject: [PATCH 12/18] Added support for @DynamoDbAutoGeneratedTimestampAttribute on attributes within nested objects --- ...-AmazonDynamoDBEnhancedClient-96aff9e.json | 2 +- .../operations/UpdateItemOperation.java | 18 +- .../update/UpdateExpressionUtils.java | 41 +- .../annotations/DynamoDbUpdateBehavior.java | 9 - .../extensions/NestedRecordUtilsTest.java | 58 --- .../functionaltests/UpdateBehaviorTest.java | 375 ++++-------------- .../NestedRecordWithUpdateBehavior.java | 29 +- .../models/RecordWithUpdateBehaviors.java | 12 +- 8 files changed, 102 insertions(+), 442 deletions(-) delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/NestedRecordUtilsTest.java diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json index 8ed0b2c84583..eecef9799cf3 100644 --- a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json +++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-96aff9e.json @@ -2,5 +2,5 @@ "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." + "description": "Added support for @DynamoDbAutoGeneratedTimestampAttribute on attributes within nested objects." } 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 3b089446736a..0ffe361b5aed 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 @@ -132,7 +132,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(tableSchema, transformation, nonKeyAttributes); + Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, nonKeyAttributes); Expression conditionExpression = generateConditionExpressionIfExist(transformation, request); Map expressionNames = coalesceExpressionNames(updateExpression, conditionExpression); @@ -171,7 +171,6 @@ public Map transformItemToMapForUpdateExpression(Map nestedAttributes = new HashMap<>(); itemToMap.forEach((key, value) -> { - validateAttributeName(key); if (value.hasM() && isNotEmptyMap(value.m())) { nestedAttributes.put(key, value); } @@ -193,9 +192,8 @@ private Map nestedItemToMap(Map String key, AttributeValue attributeValue) { attributeValue.m().forEach((mapKey, mapValue) -> { - validateAttributeName(mapKey); + String nestedAttributeKey = key + NESTED_OBJECT_UPDATE + mapKey; if (attributeValueNonNullOrShouldWriteNull(mapValue)) { - String nestedAttributeKey = key + NESTED_OBJECT_UPDATE + mapKey; if (mapValue.hasM()) { nestedItemToMap(itemToMap, nestedAttributeKey, mapValue); } else { @@ -277,7 +275,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(TableSchema tableSchema, + private Expression generateUpdateExpressionIfExist(TableMetadata tableMetadata, WriteModification transformation, Map attributes) { UpdateExpression updateExpression = null; @@ -286,7 +284,7 @@ private Expression generateUpdateExpressionIfExist(TableSchema tableSchema, } if (!attributes.isEmpty()) { List nonRemoveAttributes = UpdateExpressionConverter.findAttributeNames(updateExpression); - UpdateExpression operationUpdateExpression = operationExpression(attributes, tableSchema, nonRemoveAttributes); + UpdateExpression operationUpdateExpression = operationExpression(attributes, tableMetadata, nonRemoveAttributes); if (updateExpression == null) { updateExpression = operationUpdateExpression; } else { @@ -357,12 +355,4 @@ private static Map coalesceExpressionValues(Expression f } return expressionValues; } - - private static void validateAttributeName(String attributeName) { - if (attributeName.contains(NESTED_OBJECT_UPDATE)) { - throw new IllegalArgumentException( - String.format("Attribute name '%s' contains reserved marker '%s' and is not allowed.", - attributeName, NESTED_OBJECT_UPDATE)); - } - } } 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 4ad1989d057d..1d47400ab2e6 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,24 +15,21 @@ 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.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; 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; @@ -60,12 +57,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, - TableSchema tableSchema, + TableMetadata tableMetadata, List nonRemoveAttributes) { Map setAttributes = filterMap(itemMap, e -> !isNullAttributeValue(e.getValue())); UpdateExpression setAttributeExpression = UpdateExpression.builder() - .actions(setActionsFor(setAttributes, tableSchema)) + .actions(setActionsFor(setAttributes, tableMetadata)) .build(); Map removeAttributes = @@ -81,31 +78,13 @@ 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, 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; + 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()); } /** 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 7adc4d0006a7..fa161446c1a4 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,19 +22,10 @@ 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. - *

- * Note: This annotation must not be applied to fields whose names contain the reserved marker "_NESTED_ATTR_UPDATE_". - * This marker is used internally by the Enhanced Client to represent flattened paths for nested attribute updates. - * If a field name contains this marker, an IllegalArgumentException will be thrown during schema registration. */ @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 deleted file mode 100644 index 8855f73a719c..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/NestedRecordUtilsTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.extensions; - -import static software.amazon.awssdk.enhanced.dynamodb.internal.extensions.utility.NestedRecordUtils.getTableSchemaForListElement; -import static software.amazon.awssdk.enhanced.dynamodb.internal.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.TimestampListElement; -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(TimestampListElement.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/UpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java index a99c4b266e7a..fe87b1ece6e0 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,32 +2,24 @@ 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 static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InvalidNestedAttributeBean.InvalidNestedAttributeChild; +import static org.junit.Assert.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; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; 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.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.InvalidNestedAttributeBean; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InvalidRootAttributeBean; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedRecordWithUpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.TimestampListElement; import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver; import software.amazon.awssdk.enhanced.dynamodb.model.IgnoreNullsMode; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -43,17 +35,11 @@ public class UpdateBehaviorTest extends LocalDynamoDbSyncTestBase { private static final String TEST_ATTRIBUTE = "testAttribute"; private static final TableSchema TABLE_SCHEMA = - TableSchema.fromClass(RecordWithUpdateBehaviors.class); + TableSchema.fromClass(RecordWithUpdateBehaviors.class); private static final TableSchema TABLE_SCHEMA_FLATTEN_RECORD = TableSchema.fromClass(FlattenRecord.class); - private static final TableSchema TABLE_SCHEMA_INVALID_ATTR_NAME_ON_ROOT = - TableSchema.fromClass(InvalidRootAttributeBean.class); - - private static final TableSchema TABLE_SCHEMA_INVALID_ATTR_NAME_ON_NESTED_LEVEL = - TableSchema.fromClass(InvalidNestedAttributeBean.class); - private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()).extensions( Stream.concat(ExtensionResolver.defaultExtensions().stream(), @@ -66,15 +52,6 @@ public class UpdateBehaviorTest extends LocalDynamoDbSyncTestBase { private final DynamoDbTable flattenedMappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_FLATTEN_RECORD); - private final DynamoDbTable beanWithInvalidRootAttrNameMappedTable = - enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_INVALID_ATTR_NAME_ON_ROOT); - - private final DynamoDbTable beanWithInvalidNestedAttrNameMappedTable = - enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_INVALID_ATTR_NAME_ON_NESTED_LEVEL); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Before public void createTable() { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); @@ -88,15 +65,10 @@ public void deleteTable() { @Test public void updateBehaviors_firstUpdate() { 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); @@ -111,57 +83,28 @@ 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().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); - + Instant beforeUpdateInstant = Instant.now(); 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); @@ -172,14 +115,6 @@ 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 @@ -231,7 +166,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(); @@ -240,30 +175,21 @@ 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 updateRecord = new RecordWithUpdateBehaviors(); - updateRecord.setId("id123"); - updateRecord.setVersion(1L); - updateRecord.setNestedRecord(updatedNestedRecord); + RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); + update_record.setId("id123"); + update_record.setVersion(1L); + update_record.setNestedRecord(updatedNestedRecord); - mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, - TEST_BEHAVIOUR_ATTRIBUTE, currentTime); - assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isEqualTo(nestedCreatedTime); - assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isAfter(nestedUpdatedTime); + TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1); } @Test @@ -277,30 +203,20 @@ 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 updateRecord = new RecordWithUpdateBehaviors(); - updateRecord.setId("id123"); - updateRecord.setVersion(1L); - updateRecord.setNestedRecord(updatedNestedRecord); + RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); + update_record.setId("id123"); + update_record.setVersion(1L); + update_record.setNestedRecord(updatedNestedRecord); - mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.DEFAULT)); + mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.DEFAULT)); - persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, - currentTime); - assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isAfter(nestedCreatedTime); - assertThat(persistedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isAfter(nestedUpdatedTime); + verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, currentTime); } @Test @@ -318,78 +234,21 @@ public void when_updatingNestedObjectWithSingleLevel_with_no_mode_update_newMapC long updatedNestedCounter = 10L; updatedNestedRecord.setNestedCounter(updatedNestedCounter); - RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); - updateRecord.setId("id123"); - updateRecord.setVersion(1L); - updateRecord.setNestedRecord(updatedNestedRecord); + RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); + update_record.setId("id123"); + update_record.setVersion(1L); + update_record.setNestedRecord(updatedNestedRecord); - mappedTable.updateItem(r -> r.item(updateRecord)); + mappedTable.updateItem(r -> r.item(update_record)); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - 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); - TimestampListElement firstElement = new TimestampListElement(); - firstElement.setId("id1"); - firstElement.setAttribute("attr1"); - TimestampListElement secondElement = new TimestampListElement(); - 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); + verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, currentTime); } @Test public void when_updatingNestedObjectToEmptyWithSingleLevel_existingInformationIsPreserved_scalar_only_update() { + Instant currentTime = Instant.now().minusMillis(1); NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); nestedRecord.setAttribute(TEST_ATTRIBUTE); @@ -402,59 +261,16 @@ public void when_updatingNestedObjectToEmptyWithSingleLevel_existingInformationI NestedRecordWithUpdateBehavior updatedNestedRecord = new NestedRecordWithUpdateBehavior(); - RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); - updateRecord.setId("id123"); - updateRecord.setVersion(1L); - updateRecord.setNestedRecord(updatedNestedRecord); + RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); + update_record.setId("id123"); + update_record.setVersion(1L); + update_record.setNestedRecord(updatedNestedRecord); - mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + mappedTable.updateItem(r -> r.item(update_record).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.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); - 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); + assertThat(persistedRecord.getCreatedAutoUpdateOn()).isAfterOrEqualTo(currentTime); } private NestedRecordWithUpdateBehavior createNestedWithDefaults(String id, Long counter) { @@ -462,6 +278,7 @@ private NestedRecordWithUpdateBehavior createNestedWithDefaults(String id, Long nestedRecordWithDefaults.setId(id); nestedRecordWithDefaults.setNestedCounter(counter); nestedRecordWithDefaults.setNestedUpdateBehaviorAttribute(TEST_BEHAVIOUR_ATTRIBUTE); + nestedRecordWithDefaults.setNestedTimeAttribute(INSTANT_1); return nestedRecordWithDefaults; } @@ -469,34 +286,31 @@ private NestedRecordWithUpdateBehavior createNestedWithDefaults(String id, Long private void verifyMultipleLevelNestingTargetedUpdateBehavior(NestedRecordWithUpdateBehavior nestedRecord, long updatedOuterNestedCounter, long updatedInnerNestedCounter, - String testBehaviorAttribute, - Instant expectedTime) { + String test_behav_attribute, + Instant expected_time) { 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( - testBehaviorAttribute); - assertThat(nestedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isAfter(expectedTime); - assertThat(nestedRecord.getNestedRecord().getNestedUpdatedTimeAttribute()).isAfter(expectedTime); + test_behav_attribute); + assertThat(nestedRecord.getNestedRecord().getNestedTimeAttribute()).isAfterOrEqualTo(expected_time); } private void verifySingleLevelNestingTargetedUpdateBehavior(NestedRecordWithUpdateBehavior nestedRecord, - long updatedNestedCounter, String expectedBehaviorAttr, - Instant expectedTime) { + long updatedNestedCounter, String expected_behav_attr, + Instant expected_time) { assertThat(nestedRecord).isNotNull(); assertThat(nestedRecord.getNestedCounter()).isEqualTo(updatedNestedCounter); - assertThat(nestedRecord.getNestedUpdateBehaviorAttribute()).isEqualTo(expectedBehaviorAttr); - assertThat(nestedRecord.getNestedCreatedTimeAttribute()).isAfter(expectedTime); - assertThat(nestedRecord.getNestedUpdatedTimeAttribute()).isAfter(expectedTime); + assertThat(nestedRecord.getNestedUpdateBehaviorAttribute()).isEqualTo(expected_behav_attr); + assertThat(nestedRecord.getNestedTimeAttribute()).isAfterOrEqualTo(expected_time); } @Test public void when_updatingNestedObjectWithMultipleLevels_inScalarOnlyMode_existingInformationIsPreserved() { + NestedRecordWithUpdateBehavior nestedRecord1 = createNestedWithDefaults("id789", 50L); NestedRecordWithUpdateBehavior nestedRecord2 = createNestedWithDefaults("id456", 0L); @@ -517,12 +331,12 @@ public void when_updatingNestedObjectWithMultipleLevels_inScalarOnlyMode_existin long outerNestedCounter = 200L; updatedNestedRecord1.setNestedCounter(outerNestedCounter); - RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); - updateRecord.setId("id123"); - updateRecord.setVersion(1L); - updateRecord.setNestedRecord(updatedNestedRecord1); + RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); + update_record.setId("id123"); + update_record.setVersion(1L); + update_record.setNestedRecord(updatedNestedRecord1); - mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); @@ -532,6 +346,7 @@ public void when_updatingNestedObjectWithMultipleLevels_inScalarOnlyMode_existin @Test public void when_updatingNestedObjectWithMultipleLevels_inMapsOnlyMode_existingInformationIsPreserved() { + NestedRecordWithUpdateBehavior nestedRecord1 = createNestedWithDefaults("id789", 50L); NestedRecordWithUpdateBehavior nestedRecord2 = createNestedWithDefaults("id456", 0L); @@ -547,12 +362,12 @@ public void when_updatingNestedObjectWithMultipleLevels_inMapsOnlyMode_existingI long outerNestedCounter = 200L; updatedNestedRecord1.setNestedCounter(outerNestedCounter); - RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); - updateRecord.setId("id123"); - updateRecord.setVersion(1L); - updateRecord.setNestedRecord(updatedNestedRecord1); + RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); + update_record.setId("id123"); + update_record.setVersion(1L); + update_record.setNestedRecord(updatedNestedRecord1); - mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); + mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); @@ -583,22 +398,22 @@ public void when_updatingNestedObjectWithMultipleLevels_default_mode_existingInf long outerNestedCounter = 200L; updatedNestedRecord1.setNestedCounter(outerNestedCounter); - RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); - updateRecord.setId("id123"); - updateRecord.setVersion(1L); - updateRecord.setNestedRecord(updatedNestedRecord1); + RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); + update_record.setId("id123"); + update_record.setVersion(1L); + update_record.setNestedRecord(updatedNestedRecord1); - mappedTable.updateItem(r -> r.item(updateRecord)); + mappedTable.updateItem(r -> r.item(update_record)); RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123"))); - verifyMultipleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), outerNestedCounter, - innerNestedCounter, null, + verifyMultipleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), outerNestedCounter, innerNestedCounter, null, currentTime); } @Test public void when_updatingNestedNonScalarObject_scalar_only_update_throwsDynamoDBException() { + NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); nestedRecord.setAttribute(TEST_ATTRIBUTE); @@ -607,34 +422,35 @@ public void when_updatingNestedNonScalarObject_scalar_only_update_throwsDynamoDB mappedTable.putItem(record); - RecordWithUpdateBehaviors updateRecord = new RecordWithUpdateBehaviors(); - updateRecord.setId("id123"); - updateRecord.setVersion(1L); - updateRecord.setKey("abc"); - updateRecord.setNestedRecord(nestedRecord); + RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); + update_record.setId("id123"); + update_record.setVersion(1L); + update_record.setKey("abc"); + update_record.setNestedRecord(nestedRecord); - assertThatThrownBy(() -> mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY))) + assertThatThrownBy(() -> mappedTable.updateItem(r -> r.item(update_record).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 updateRecord = new RecordWithUpdateBehaviors(); - updateRecord.setId("id123"); - updateRecord.setVersion(1L); - updateRecord.setKey("abc"); + RecordWithUpdateBehaviors update_record = new RecordWithUpdateBehaviors(); + update_record.setId("id123"); + update_record.setVersion(1L); + update_record.setKey("abc"); NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id456", 5L); nestedRecord.setAttribute(TEST_ATTRIBUTE); - updateRecord.setNestedRecord(nestedRecord); + update_record.setNestedRecord(nestedRecord); RecordWithUpdateBehaviors persistedRecord = - mappedTable.updateItem(r -> r.item(updateRecord).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); + mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY)); verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), 5L, TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1); @@ -659,8 +475,7 @@ public void when_emptyNestedRecordIsSet_emptyMapIsStoredInTable() { assertThat(getItemResponse.item().get("nestedRecord")).isNotNull(); Map nestedRecord = getItemResponse.item().get("nestedRecord").m(); - assertThat(nestedRecord.get("nestedCreatedTimeAttribute")).isNotNull(); - assertThat(nestedRecord.get("nestedUpdatedTimeAttribute")).isNotNull(); + assertThat(nestedRecord.get("nestedTimeAttribute")).isNotNull(); assertTrue(nestedRecord.get("id").nul()); assertTrue(nestedRecord.get("nestedRecord").nul()); assertTrue(nestedRecord.get("attribute").nul()); @@ -672,6 +487,7 @@ public void when_emptyNestedRecordIsSet_emptyMapIsStoredInTable() { @Test public void when_updatingNestedObjectWithSingleLevelFlattened_existingInformationIsPreserved_scalar_only_update() { + NestedRecordWithUpdateBehavior nestedRecord = createNestedWithDefaults("id123", 10L); CompositeRecord compositeRecord = new CompositeRecord(); @@ -701,8 +517,11 @@ public void when_updatingNestedObjectWithSingleLevelFlattened_existingInformatio 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); @@ -739,14 +558,14 @@ 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 (only the timestamp). + * Currently, nested records are not updated through extensions. */ @Test public void updateBehaviors_nested() { + Instant currentTime = Instant.now().minusMillis(1); NestedRecordWithUpdateBehavior nestedRecord = new NestedRecordWithUpdateBehavior(); nestedRecord.setId("id456"); @@ -764,40 +583,6 @@ public void updateBehaviors_nested() { assertThat(persistedRecord.getNestedRecord().getNestedVersionedAttribute()).isNull(); assertThat(persistedRecord.getNestedRecord().getNestedCounter()).isNull(); assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isNull(); - assertThat(persistedRecord.getNestedRecord().getNestedCreatedTimeAttribute()).isNotNull(); - } - - @Test - public void updateBehaviour_onItemWithRootAttributeNameContainingReservedMarker_throwsException() { - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Attribute name 'attr_NESTED_ATTR_UPDATE_' contains reserved marker " - + "'_NESTED_ATTR_UPDATE_' and is not allowed."); - - InvalidRootAttributeBean record = new InvalidRootAttributeBean(); - record.setId("1"); - record.setAttr_NESTED_ATTR_UPDATE_(INSTANT_1); - - beanWithInvalidRootAttrNameMappedTable.updateItem(r -> r.item(record) - .ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); - } - - @Test - public void updateBehaviour_onItemWithNestedAttributeNameContainingReservedMarker_throwsException() { - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Attribute name 'attr_NESTED_ATTR_UPDATE_' contains reserved marker " - + "'_NESTED_ATTR_UPDATE_' and is not allowed."); - - InvalidNestedAttributeBean record = new InvalidNestedAttributeBean(); - record.setId("1"); - - InvalidNestedAttributeChild childBean = new InvalidNestedAttributeChild(); - childBean.setId("2"); - childBean.setAttr_NESTED_ATTR_UPDATE_(INSTANT_1); - record.setNestedChildAttribute(childBean); - - beanWithInvalidNestedAttrNameMappedTable.updateItem(r -> r.item(record) - .ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY)); + assertThat(persistedRecord.getNestedRecord().getNestedTimeAttribute()).isAfterOrEqualTo(currentTime); } } 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 a2bcdc4dd690..883a89813c1a 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,7 +18,6 @@ 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; @@ -31,12 +30,10 @@ public class NestedRecordWithUpdateBehavior { private String id; private String nestedUpdateBehaviorAttribute; private Long nestedVersionedAttribute; - private Instant nestedCreatedTimeAttribute; - private Instant nestedUpdatedTimeAttribute; + private Instant nestedTimeAttribute; private Long nestedCounter; private NestedRecordWithUpdateBehavior nestedRecord; private String attribute; - private List nestedRecordList; @DynamoDbPartitionKey public String getId() { @@ -66,22 +63,12 @@ public void setNestedVersionedAttribute(Long nestedVersionedAttribute) { } @DynamoDbAutoGeneratedTimestampAttribute - @DynamoDbUpdateBehavior(WRITE_IF_NOT_EXISTS) - public Instant getNestedCreatedTimeAttribute() { - return nestedCreatedTimeAttribute; + public Instant getNestedTimeAttribute() { + return nestedTimeAttribute; } - public void setNestedCreatedTimeAttribute(Instant nestedCreatedTimeAttribute) { - this.nestedCreatedTimeAttribute = nestedCreatedTimeAttribute; - } - - @DynamoDbAutoGeneratedTimestampAttribute - public Instant getNestedUpdatedTimeAttribute() { - return nestedUpdatedTimeAttribute; - } - - public void setNestedUpdatedTimeAttribute(Instant nestedUpdatedTimeAttribute) { - this.nestedUpdatedTimeAttribute = nestedUpdatedTimeAttribute; + public void setNestedTimeAttribute(Instant nestedTimeAttribute) { + this.nestedTimeAttribute = nestedTimeAttribute; } @DynamoDbAtomicCounter @@ -108,10 +95,4 @@ 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 202f7ee13ac7..8bd874fee002 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 @@ -15,10 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; -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.converters.EpochMillisFormatTestConverter; import software.amazon.awssdk.enhanced.dynamodb.converters.TimeFormatUpdateTestConverter; import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute; @@ -29,6 +26,8 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS; + @DynamoDbBean public class RecordWithUpdateBehaviors { private String id; @@ -41,7 +40,6 @@ public class RecordWithUpdateBehaviors { private Instant formattedLastAutoUpdatedOn; private NestedRecordWithUpdateBehavior nestedRecord; private String key; - private List nestedRecordList; @DynamoDbPartitionKey public String getId() { @@ -135,10 +133,4 @@ public NestedRecordWithUpdateBehavior getNestedRecord() { public void setNestedRecord(NestedRecordWithUpdateBehavior nestedRecord) { this.nestedRecord = nestedRecord; } - - public List getNestedRecordList() { return nestedRecordList;} - - public void setNestedRecordList(List nestedRecordList) { - this.nestedRecordList = nestedRecordList; - } } From e1636a7d3b1bf6294ecbf85de2333b5437631b9e Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Wed, 4 Feb 2026 10:41:29 +0200 Subject: [PATCH 13/18] Added support for @DynamoDbAutoGeneratedTimestampAttribute on attributes within nested objects --- .../AutoGeneratedTimestampExtensionTest.java | 38 ++++---- .../AutogeneratedTimestampTestModels.java | 95 +++++++++++++++++++ .../models/InvalidNestedAttributeBean.java | 89 ----------------- .../models/InvalidNestedAttributeRecord.java | 79 --------------- .../models/InvalidRootAttributeBean.java | 50 ---------- .../models/InvalidRootAttributeRecord.java | 42 -------- .../models/TimestampListElement.java | 54 ----------- 7 files changed, 114 insertions(+), 333 deletions(-) delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeBean.java delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeRecord.java delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeBean.java delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeRecord.java delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/TimestampListElement.java diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java index c9dd1d9bb7e5..798dd052a07a 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java @@ -63,8 +63,8 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithMap; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithSet; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleStaticRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InvalidNestedAttributeRecord; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InvalidRootAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithInvalidNestedAttributeName; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithInvalidRootAttributeName; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; @@ -724,17 +724,17 @@ public void autogenerateTimestamps_onRootAttributeWithReservedMarker_throwsExcep + "'_NESTED_ATTR_UPDATE_' and is not allowed."); StaticTableSchema - .builder(InvalidRootAttributeRecord.class) - .newItemSupplier(InvalidRootAttributeRecord::new) + .builder(BeanWithInvalidRootAttributeName.class) + .newItemSupplier(BeanWithInvalidRootAttributeName::new) .addAttribute(String.class, a -> a.name("id") - .getter(InvalidRootAttributeRecord::getId) - .setter(InvalidRootAttributeRecord::setId) + .getter(BeanWithInvalidRootAttributeName::getId) + .setter(BeanWithInvalidRootAttributeName::setId) .tags(primaryPartitionKey())) .addAttribute(Instant.class, a -> a.name("attr_NESTED_ATTR_UPDATE_") - .getter(InvalidRootAttributeRecord::getAttr_NESTED_ATTR_UPDATE_) - .setter(InvalidRootAttributeRecord::setAttr_NESTED_ATTR_UPDATE_) + .getter(BeanWithInvalidRootAttributeName::getAttr_NESTED_ATTR_UPDATE_) + .setter(BeanWithInvalidRootAttributeName::setAttr_NESTED_ATTR_UPDATE_) .tags(autoGeneratedTimestampAttribute())) .build(); } @@ -746,29 +746,29 @@ public void autogenerateTimestamps_onNestedAttributeWithReservedMarker_throwsExc + "'_NESTED_ATTR_UPDATE_' and is not allowed."); StaticTableSchema - .builder(InvalidNestedAttributeRecord.class) - .newItemSupplier(InvalidNestedAttributeRecord::new) + .builder(BeanWithInvalidNestedAttributeName.class) + .newItemSupplier(BeanWithInvalidNestedAttributeName::new) .addAttribute( String.class, a -> a.name("id") - .getter(InvalidNestedAttributeRecord::getId) - .setter(InvalidNestedAttributeRecord::setId) + .getter(BeanWithInvalidNestedAttributeName::getId) + .setter(BeanWithInvalidNestedAttributeName::setId) .tags(primaryPartitionKey())) .addAttribute( EnhancedType.documentOf( - InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild.class, + BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild.class, StaticTableSchema - .builder(InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild.class) - .newItemSupplier(InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild::new) + .builder(BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild.class) + .newItemSupplier(BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild::new) .addAttribute(Instant.class, a -> a.name("childAttr_NESTED_ATTR_UPDATE_") - .getter(InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild::getAttr_NESTED_ATTR_UPDATE_) - .setter(InvalidNestedAttributeRecord.InvalidNestedAttributeRecordChild::setAttr_NESTED_ATTR_UPDATE_) + .getter(BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild::getAttr_NESTED_ATTR_UPDATE_) + .setter(BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild::setAttr_NESTED_ATTR_UPDATE_) .tags(autoGeneratedTimestampAttribute())) .build()), a -> a.name("nestedChildAttribute") - .getter(InvalidNestedAttributeRecord::getNestedChildAttribute) - .setter(InvalidNestedAttributeRecord::setNestedChildAttribute)) + .getter(BeanWithInvalidNestedAttributeName::getNestedChildAttribute) + .setter(BeanWithInvalidNestedAttributeName::setNestedChildAttribute)) .build(); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java index 8b8e1520f934..83cdcc6af600 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java @@ -895,4 +895,99 @@ public static TableSchema buildStaticImmutableSch .setter(NestedImmutableRecordWithList.Builder::level2)) .build(); } + + /** + * Test model with an invalid root-level attribute name containing the reserved '_NESTED_ATTR_UPDATE_' pattern. + * Used to test validation of attribute names that conflict with internal DynamoDB Enhanced Client conventions. + */ + @DynamoDbBean + public static class BeanWithInvalidRootAttributeName { + private String id; + private Instant attr_NESTED_ATTR_UPDATE_; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public BeanWithInvalidRootAttributeName setId(String id) { + this.id = id; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getAttr_NESTED_ATTR_UPDATE_() { + return attr_NESTED_ATTR_UPDATE_; + } + + public BeanWithInvalidRootAttributeName setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + this.attr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; + return this; + } + } + + /** + * Test model with an invalid nested attribute name containing the reserved '_NESTED_ATTR_UPDATE_' pattern. + * Used to test validation of nested attribute names that conflict with internal DynamoDB Enhanced Client conventions. + */ + @DynamoDbBean + public static class BeanWithInvalidNestedAttributeName { + private String id; + private BeanWithInvalidNestedAttributeNameChild nestedChildAttribute; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public BeanWithInvalidNestedAttributeName setId(String id) { + this.id = id; + return this; + } + + public BeanWithInvalidNestedAttributeNameChild getNestedChildAttribute() { + return nestedChildAttribute; + } + + public BeanWithInvalidNestedAttributeName setNestedChildAttribute(BeanWithInvalidNestedAttributeNameChild nestedChildAttribute) { + this.nestedChildAttribute = nestedChildAttribute; + return this; + } + + @DynamoDbBean + public static class BeanWithInvalidNestedAttributeNameChild { + private String id; + private BeanWithInvalidNestedAttributeNameChild nestedChildAttribute; + private Instant childAttr_NESTED_ATTR_UPDATE_; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public BeanWithInvalidNestedAttributeNameChild setId(String id) { + this.id = id; + return this; + } + + public BeanWithInvalidNestedAttributeNameChild getNestedChildAttribute() { + return nestedChildAttribute; + } + + public BeanWithInvalidNestedAttributeNameChild setNestedChildAttribute(BeanWithInvalidNestedAttributeNameChild nestedChildAttribute) { + this.nestedChildAttribute = nestedChildAttribute; + return this; + } + + @DynamoDbAutoGeneratedTimestampAttribute + public Instant getAttr_NESTED_ATTR_UPDATE_() { + return childAttr_NESTED_ATTR_UPDATE_; + } + + public BeanWithInvalidNestedAttributeNameChild setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { + this.childAttr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; + return this; + } + } + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeBean.java deleted file mode 100644 index 5e3da3dd1959..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeBean.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; - -import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS; - -import java.time.Instant; -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.DynamoDbUpdateBehavior; - -@DynamoDbBean -public class InvalidNestedAttributeBean { - - private String id; - private InvalidNestedAttributeChild nestedChildAttribute; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public InvalidNestedAttributeBean setId(String id) { - this.id = id; - return this; - } - - public InvalidNestedAttributeChild getNestedChildAttribute() { - return nestedChildAttribute; - } - - public InvalidNestedAttributeBean setNestedChildAttribute( - InvalidNestedAttributeChild nestedChildAttribute) { - this.nestedChildAttribute = nestedChildAttribute; - return this; - } - - - @DynamoDbBean - public static class InvalidNestedAttributeChild { - - private String id; - private InvalidNestedAttributeChild nestedChildAttribute; - private Instant childAttr_NESTED_ATTR_UPDATE_; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public InvalidNestedAttributeChild setId(String id) { - this.id = id; - return this; - } - - public InvalidNestedAttributeChild getNestedChildAttribute() { - return nestedChildAttribute; - } - - public InvalidNestedAttributeChild setNestedChildAttribute( - InvalidNestedAttributeChild nestedChildAttribute) { - this.nestedChildAttribute = nestedChildAttribute; - return this; - } - - @DynamoDbUpdateBehavior(WRITE_IF_NOT_EXISTS) - public Instant getAttr_NESTED_ATTR_UPDATE_() { - return childAttr_NESTED_ATTR_UPDATE_; - } - - public InvalidNestedAttributeChild setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { - this.childAttr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; - return this; - } - } -} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeRecord.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeRecord.java deleted file mode 100644 index 7d2bac5a8a9f..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidNestedAttributeRecord.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; - -import java.time.Instant; - -public class InvalidNestedAttributeRecord { - - private String id; - private InvalidNestedAttributeRecordChild nestedChildAttribute; - - public String getId() { - return id; - } - - public InvalidNestedAttributeRecord setId(String id) { - this.id = id; - return this; - } - - public InvalidNestedAttributeRecordChild getNestedChildAttribute() { - return nestedChildAttribute; - } - - public InvalidNestedAttributeRecord setNestedChildAttribute( - InvalidNestedAttributeRecordChild nestedChildAttribute) { - this.nestedChildAttribute = nestedChildAttribute; - return this; - } - - - public static class InvalidNestedAttributeRecordChild { - - private String id; - private InvalidNestedAttributeRecordChild nestedChildAttribute; - private Instant childAttr_NESTED_ATTR_UPDATE_; - - public String getId() { - return id; - } - - public InvalidNestedAttributeRecordChild setId(String id) { - this.id = id; - return this; - } - - public InvalidNestedAttributeRecordChild getNestedChildAttribute() { - return nestedChildAttribute; - } - - public InvalidNestedAttributeRecordChild setNestedChildAttribute( - InvalidNestedAttributeRecordChild nestedChildAttribute) { - this.nestedChildAttribute = nestedChildAttribute; - return this; - } - - public Instant getAttr_NESTED_ATTR_UPDATE_() { - return childAttr_NESTED_ATTR_UPDATE_; - } - - public InvalidNestedAttributeRecordChild setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { - this.childAttr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; - return this; - } - } -} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeBean.java deleted file mode 100644 index 3efd7597ff70..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeBean.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; - -import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS; - -import java.time.Instant; -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.DynamoDbUpdateBehavior; - -@DynamoDbBean -public class InvalidRootAttributeBean { - - private String id; - private Instant attr_NESTED_ATTR_UPDATE_; - - @DynamoDbPartitionKey - public String getId() { - return id; - } - - public InvalidRootAttributeBean setId(String id) { - this.id = id; - return this; - } - - @DynamoDbUpdateBehavior(WRITE_IF_NOT_EXISTS) - public Instant getAttr_NESTED_ATTR_UPDATE_() { - return attr_NESTED_ATTR_UPDATE_; - } - - public InvalidRootAttributeBean setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { - this.attr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; - return this; - } -} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeRecord.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeRecord.java deleted file mode 100644 index 813641ee4d3d..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InvalidRootAttributeRecord.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; - -import java.time.Instant; - -public class InvalidRootAttributeRecord { - - private String id; - private Instant attr_NESTED_ATTR_UPDATE_; - - public String getId() { - return id; - } - - public InvalidRootAttributeRecord setId(String id) { - this.id = id; - return this; - } - - public Instant getAttr_NESTED_ATTR_UPDATE_() { - return attr_NESTED_ATTR_UPDATE_; - } - - public InvalidRootAttributeRecord setAttr_NESTED_ATTR_UPDATE_(Instant attr_NESTED_ATTR_UPDATE_) { - this.attr_NESTED_ATTR_UPDATE_ = attr_NESTED_ATTR_UPDATE_; - return this; - } -} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/TimestampListElement.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/TimestampListElement.java deleted file mode 100644 index 23e7abe9d94c..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/TimestampListElement.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.enhanced.dynamodb.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 TimestampListElement { - 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; - } -} \ No newline at end of file From c9c078755b8a70f6715fae57fcecec37321904bf Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 6 Feb 2026 11:43:01 +0200 Subject: [PATCH 14/18] Added support for @DynamoDbAutoGeneratedTimestampAttribute on attributes within nested objects --- ...AutoGeneratedTimestampRecordExtension.java | 22 +- .../internal/EnhancedClientUtils.java | 37 ++- .../extensions/utility/NestedRecordUtils.java | 42 ++- .../internal/EnhancedClientUtilsTest.java | 160 +++++++++++- .../utility/NestedRecordUtilsTest.java | 241 ++++++++++++++++++ 5 files changed, 477 insertions(+), 25 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtilsTest.java 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 2026bc932211..afc41280df6c 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 @@ -206,8 +206,13 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex if (customMetadataObject != null) { customMetadataObject.forEach( - key -> insertTimestampInItemToTransform(updatedItems, reconstructCompositeKey(path, key), - schema.converterForAttribute(key), currentInstant)); + key -> { + AttributeConverter converter = schema.converterForAttribute(key); + if (converter != null) { + insertTimestampInItemToTransform(updatedItems, reconstructCompositeKey(path, key), + converter, currentInstant); + } + }); } }); @@ -245,12 +250,15 @@ private Map processNestedObject(Map(nestedMap); - updated = true; + AttributeConverter converter = nestedSchema.converterForAttribute(key); + if (converter != null) { + if (!updated) { + updatedNestedMap = new HashMap<>(nestedMap); + updated = true; + } + insertTimestampInItemToTransform(updatedNestedMap, String.valueOf(key), + converter, currentInstant); } - insertTimestampInItemToTransform(updatedNestedMap, String.valueOf(key), - nestedSchema.converterForAttribute(key), currentInstant); } } 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 3787e12a9a34..fd7fde941d5a 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 @@ -28,6 +28,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.Key; @@ -38,6 +39,8 @@ import software.amazon.awssdk.enhanced.dynamodb.model.Page; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.awssdk.utils.StringUtils; @SdkInternalApi public final class EnhancedClientUtils { @@ -207,19 +210,33 @@ public static boolean isNullAttributeValue(AttributeValue attributeValue) { } /** - * Retrieves the {@link TableSchema} for a nested attribute within the given parent schema. When the attribute is a - * parameterized type (e.g., List), it retrieves the schema of the first type parameter. Otherwise, it retrieves the schema - * directly from the attribute's enhanced type. + * Retrieves the nested {@link TableSchema} for an attribute from the parent schema. + * For parameterized types (e.g., Set, List, Map), extracts the first type parameter's 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 + * @param parentSchema the parent schema; must not be null + * @param attributeName the attribute name; must not be null or empty + * @return the nested schema, or empty if unavailable */ - public static Optional> getNestedSchema(TableSchema parentSchema, String attributeName) { - EnhancedType enhancedType = parentSchema.converterForAttribute(attributeName).type(); - List> rawClassParameters = enhancedType.rawClassParameters(); + public static Optional> getNestedSchema(TableSchema parentSchema, CharSequence attributeName) { + if (parentSchema == null) { + throw new IllegalArgumentException("Parent schema cannot be null."); + } + if (StringUtils.isEmpty(attributeName)) { + throw new IllegalArgumentException("Attribute name cannot be null or empty."); + } + + AttributeConverter converter = parentSchema.converterForAttribute(attributeName); + if (converter == null) { + return Optional.empty(); + } + + EnhancedType enhancedType = converter.type(); + if (enhancedType == null) { + return Optional.empty(); + } - if (rawClassParameters != null && !rawClassParameters.isEmpty()) { + List> rawClassParameters = enhancedType.rawClassParameters(); + if (!CollectionUtils.isNullOrEmpty(rawClassParameters)) { enhancedType = rawClassParameters.get(0); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtils.java index 3bb378e2e6f1..0cdfa3577922 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtils.java @@ -19,12 +19,17 @@ import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE; import java.util.HashMap; +import java.util.List; 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.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.awssdk.utils.StringUtils; @SdkInternalApi public final class NestedRecordUtils { @@ -35,7 +40,7 @@ private NestedRecordUtils() { } /** - * Resolves and returns the {@link TableSchema} for the element type of a list attribute from the provided root schema. + * Resolves and returns the {@link TableSchema} for the element type of 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. @@ -54,11 +59,20 @@ public static TableSchema getTableSchemaForListElement(TableSchema rootSch try { if (!key.contains(NESTED_OBJECT_UPDATE)) { Optional> staticSchema = getNestedSchema(rootSchema, key); - listElementSchema = - staticSchema.isPresent() - ? staticSchema.get() - : TableSchema.fromClass(Class.forName( - rootSchema.converterForAttribute(key).type().rawClassParameters().get(0).rawClass().getName())); + if (staticSchema.isPresent()) { + listElementSchema = staticSchema.get(); + } else { + AttributeConverter converter = rootSchema.converterForAttribute(key); + if (converter == null) { + throw new IllegalArgumentException("No converter found for attribute: " + key); + } + List> rawClassParameters = converter.type().rawClassParameters(); + if (CollectionUtils.isNullOrEmpty(rawClassParameters)) { + throw new IllegalArgumentException("No type parameters found for list attribute: " + key); + } + listElementSchema = TableSchema.fromClass( + Class.forName(rawClassParameters.get(0).rawClass().getName())); + } } else { String[] parts = NESTED_OBJECT_PATTERN.split(key); @@ -130,10 +144,24 @@ public static Map> resolveSchemasPerPath(Map mockSchema; + + @Mock + private AttributeConverter mockConverter; + + @Mock + private EnhancedType mockEnhancedType; + + @Mock + private EnhancedType mockParameterType; + + @Mock + private TableSchema mockNestedSchema; + @Test public void createKeyFromMap_partitionOnly() { Map itemMap = new HashMap<>(); @@ -64,4 +90,136 @@ public void cleanAttributeName_cleansSpecialCharacters() { assertThat(result).isEqualTo("a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u"); } + + @Test + public void getNestedSchema_withNullConverter_returnsEmpty() { + when(mockSchema.converterForAttribute("nonExistentAttribute")).thenReturn(null); + + Optional> result = + EnhancedClientUtils.getNestedSchema(mockSchema, "nonExistentAttribute"); + + assertThat(result).isEmpty(); + } + + @Test + public void getNestedSchema_withNullParentSchema_throwsIllegalArgumentException() { + assertThatThrownBy(() -> EnhancedClientUtils.getNestedSchema(null, "attributeName")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Parent schema cannot be null"); + } + + @Test + public void getNestedSchema_withNullAttributeName_throwsIllegalArgumentException() { + assertThatThrownBy(() -> EnhancedClientUtils.getNestedSchema(mockSchema, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Attribute name cannot be null or empty"); + } + + @Test + public void getNestedSchema_withEmptyAttributeName_throwsIllegalArgumentException() { + assertThatThrownBy(() -> EnhancedClientUtils.getNestedSchema(mockSchema, "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Attribute name cannot be null or empty"); + } + + @Test + public void getNestedSchema_withWhitespaceAttributeName_doesNotThrow() { + when(mockSchema.converterForAttribute(" ")).thenReturn(null); + + Optional> result = EnhancedClientUtils.getNestedSchema(mockSchema, " "); + + assertThat(result).isEmpty(); + } + + @Test + public void getNestedSchema_withNullEnhancedType_returnsEmpty() { + when(mockSchema.converterForAttribute("attributeWithNullType")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(null); + + Optional> result = + EnhancedClientUtils.getNestedSchema(mockSchema, "attributeWithNullType"); + + assertThat(result).isEmpty(); + } + + @Test + public void getNestedSchema_withParameterizedType_extractsFirstParameter() { + List> parameters = Collections.singletonList(mockParameterType); + when(mockSchema.converterForAttribute("listAttribute")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(mockEnhancedType); + when(mockEnhancedType.rawClassParameters()).thenReturn(parameters); + when(mockParameterType.tableSchema()).thenReturn(Optional.of(mockNestedSchema)); + + Optional> result = EnhancedClientUtils.getNestedSchema(mockSchema, "listAttribute"); + + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(mockNestedSchema); + } + + @Test + public void getNestedSchema_withEmptyParameters_usesOriginalType() { + when(mockSchema.converterForAttribute("simpleAttribute")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(mockEnhancedType); + when(mockEnhancedType.rawClassParameters()).thenReturn(Collections.emptyList()); + when(mockEnhancedType.tableSchema()).thenReturn(Optional.of(mockNestedSchema)); + + Optional> result = + EnhancedClientUtils.getNestedSchema(mockSchema, "simpleAttribute"); + + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(mockNestedSchema); + } + + @Test + public void getNestedSchema_withNullParameters_usesOriginalType() { + when(mockSchema.converterForAttribute("simpleAttribute")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(mockEnhancedType); + when(mockEnhancedType.rawClassParameters()).thenReturn(null); + when(mockEnhancedType.tableSchema()).thenReturn(Optional.of(mockNestedSchema)); + + Optional> result = + EnhancedClientUtils.getNestedSchema(mockSchema, "simpleAttribute"); + + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(mockNestedSchema); + } + + @Test + public void getNestedSchema_withNoTableSchema_returnsEmpty() { + when(mockSchema.converterForAttribute("attributeWithoutSchema")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(mockEnhancedType); + when(mockEnhancedType.rawClassParameters()).thenReturn(Collections.emptyList()); + when(mockEnhancedType.tableSchema()).thenReturn(Optional.empty()); + + Optional> result = EnhancedClientUtils.getNestedSchema(mockSchema, "attributeWithoutSchema"); + + assertThat(result).isEmpty(); + } + + @Test + public void getNestedSchema_withParameterizedTypeNoTableSchema_returnsEmpty() { + List> parameters = Collections.singletonList(mockParameterType); + when(mockSchema.converterForAttribute("listAttributeNoSchema")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(mockEnhancedType); + when(mockEnhancedType.rawClassParameters()).thenReturn(parameters); + when(mockParameterType.tableSchema()).thenReturn(Optional.empty()); + + Optional> result = + EnhancedClientUtils.getNestedSchema(mockSchema, "listAttributeNoSchema"); + + assertThat(result).isEmpty(); + } + + @Test + public void getNestedSchema_withValidInputs_returnsNestedSchema() { + when(mockSchema.converterForAttribute("validAttribute")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(mockEnhancedType); + when(mockEnhancedType.rawClassParameters()).thenReturn(Collections.emptyList()); + when(mockEnhancedType.tableSchema()).thenReturn(Optional.of(mockNestedSchema)); + + Optional> result = EnhancedClientUtils.getNestedSchema(mockSchema, "validAttribute"); + + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(mockNestedSchema); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtilsTest.java new file mode 100644 index 000000000000..9f8dbf9d831f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtilsTest.java @@ -0,0 +1,241 @@ +/* + * 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.utility; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@RunWith(MockitoJUnitRunner.class) +public class NestedRecordUtilsTest { + + private static final String NESTED_OBJECT_UPDATE = "_NESTED_ATTR_UPDATE_"; + private static final Pattern PATTERN = Pattern.compile(NESTED_OBJECT_UPDATE); + private static final Pattern NESTED_ATTR_UPDATE_ = Pattern.compile("_NESTED_ATTR_UPDATE_"); + + @Mock + private TableSchema mockSchema; + + @Mock + private AttributeConverter mockConverter; + + @Mock + private EnhancedType mockType; + + @Test + public void getTableSchemaForListElement_withNullConverter_throwsIllegalArgumentException() { + when(mockSchema.converterForAttribute("nonExistentAttribute")).thenReturn(null); + + assertThatThrownBy(() -> NestedRecordUtils.getTableSchemaForListElement(mockSchema, "nonExistentAttribute")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("No converter found for attribute: nonExistentAttribute"); + } + + @Test + public void getTableSchemaForListElement_withEmptyRawClassParameters_throwsIllegalArgumentException() { + when(mockSchema.converterForAttribute("emptyParamsAttribute")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(mockType); + when(mockType.rawClassParameters()).thenReturn(Collections.emptyList()); + + assertThatThrownBy(() -> NestedRecordUtils.getTableSchemaForListElement(mockSchema, "emptyParamsAttribute")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("No type parameters found for list attribute: emptyParamsAttribute"); + } + + @Test + public void getTableSchemaForListElement_withNullRawClassParameters_throwsIllegalArgumentException() { + when(mockSchema.converterForAttribute("nullParamsAttribute")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(mockType); + when(mockType.rawClassParameters()).thenReturn(null); + + assertThatThrownBy(() -> NestedRecordUtils.getTableSchemaForListElement(mockSchema, "nullParamsAttribute")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("No type parameters found for list attribute: nullParamsAttribute"); + } + + @Test + public void getTableSchemaForListElement_withDeepNestedPath_returnsCorrectSchema() { + String nestedKey = "nestedItem" + NESTED_OBJECT_UPDATE + "tags"; + String[] parts = PATTERN.split(nestedKey); + + assertThat(parts).hasSize(2); + assertThat(parts[0]).isEqualTo("nestedItem"); + assertThat(parts[1]).isEqualTo("tags"); + + String deepNestedKey = String.join(NESTED_OBJECT_UPDATE, "parent", "child", "grandchild"); + String[] deepParts = PATTERN.split(deepNestedKey); + + assertThat(deepParts).hasSize(3); + assertThat(deepParts[0]).isEqualTo("parent"); + assertThat(deepParts[1]).isEqualTo("child"); + assertThat(deepParts[2]).isEqualTo("grandchild"); + } + + @Test + public void resolveSchemasPerPath_withEmptyAttributeMap_returnsOnlyRootSchema() { + Map emptyAttributes = new HashMap<>(); + + Map> result = NestedRecordUtils.resolveSchemasPerPath(emptyAttributes, mockSchema); + + assertThat(result).hasSize(1); + assertThat(result).containsKey(""); + assertThat(result.get("")).isEqualTo(mockSchema); + } + + @Test + public void resolveSchemasPerPath_withFlatAttributes_returnsOnlyRootSchema() { + Map flatAttributes = new HashMap<>(); + flatAttributes.put("id", AttributeValue.builder().s("test-id").build()); + flatAttributes.put("simpleAttribute", AttributeValue.builder().s("test-value").build()); + + Map> result = NestedRecordUtils.resolveSchemasPerPath(flatAttributes, mockSchema); + + assertThat(result).hasSize(1); + assertThat(result).containsKey(""); + assertThat(result.get("")).isEqualTo(mockSchema); + } + + @Test + public void resolveSchemasPerPath_withNestedAttributes_returnsCorrectSchemas() { + Map nestedAttributes = new HashMap<>(); + nestedAttributes.put("id", AttributeValue.builder().s("test-id").build()); + nestedAttributes.put("nestedItem" + NESTED_OBJECT_UPDATE + "name", + AttributeValue.builder().s("nested-name").build()); + nestedAttributes.put("nestedItem" + NESTED_OBJECT_UPDATE + "tags", + AttributeValue.builder().ss("tag1", "tag2").build()); + + Map> result = NestedRecordUtils.resolveSchemasPerPath(nestedAttributes, mockSchema); + + assertThat(result).containsKey(""); + assertThat(result.get("")).isEqualTo(mockSchema); + + boolean hasNestedPath = nestedAttributes.keySet().stream() + .anyMatch(key -> key.contains(NESTED_OBJECT_UPDATE)); + assertThat(hasNestedPath).isTrue(); + } + + @Test + public void resolveSchemasPerPath_withMultipleNestedPaths_returnsAllSchemas() { + Map multipleNestedAttributes = new HashMap<>(); + multipleNestedAttributes.put("nestedItem" + NESTED_OBJECT_UPDATE + "name", + AttributeValue.builder().s("nested-name").build()); + multipleNestedAttributes.put("nestedItem" + NESTED_OBJECT_UPDATE + "tags", + AttributeValue.builder().ss("tag1", "tag2").build()); + + Map> result = NestedRecordUtils.resolveSchemasPerPath(multipleNestedAttributes, mockSchema); + + assertThat(result).containsKey(""); + assertThat(result.get("")).isEqualTo(mockSchema); + + String[] parts1 = NESTED_ATTR_UPDATE_.split("nestedItem_NESTED_ATTR_UPDATE_name"); + String[] parts2 = NESTED_ATTR_UPDATE_.split("nestedItem_NESTED_ATTR_UPDATE_tags"); + + assertThat(parts1[0]).isEqualTo("nestedItem"); + assertThat(parts2[0]).isEqualTo("nestedItem"); + assertThat(parts1[0]).isEqualTo(parts2[0]); // Same nested path + } + + @Test + public void resolveSchemasPerPath_withDuplicateNestedPaths_doesNotDuplicateSchemas() { + Map duplicateNestedAttributes = new HashMap<>(); + duplicateNestedAttributes.put("nestedItem" + NESTED_OBJECT_UPDATE + "name", + AttributeValue.builder().s("nested-name-1").build()); + duplicateNestedAttributes.put("nestedItem" + NESTED_OBJECT_UPDATE + "tags", + AttributeValue.builder().ss("tag1").build()); + + Map> result = NestedRecordUtils.resolveSchemasPerPath(duplicateNestedAttributes, mockSchema); + + // Assert - Should have root schema + assertThat(result).containsKey(""); + assertThat(result.get("")).isEqualTo(mockSchema); + + // Verify both attributes share the same nested path + String path1 = "nestedItem" + NESTED_OBJECT_UPDATE + "name"; + String path2 = "nestedItem" + NESTED_OBJECT_UPDATE + "tags"; + + String[] parts1 = PATTERN.split(path1); + String[] parts2 = PATTERN.split(path2); + + assertThat(parts1[0]).isEqualTo(parts2[0]); // Same parent path + assertThat(parts1[0]).isEqualTo("nestedItem"); + } + + @Test + public void reconstructCompositeKey_withNullPath_returnsAttributeName() { + String result = NestedRecordUtils.reconstructCompositeKey(null, "attributeName"); + + assertThat(result).isEqualTo("attributeName"); + } + + @Test + public void reconstructCompositeKey_withEmptyPath_returnsAttributeName() { + String result = NestedRecordUtils.reconstructCompositeKey("", "attributeName"); + + assertThat(result).isEqualTo("attributeName"); + } + + @Test + public void reconstructCompositeKey_withNullAttributeName_throwsIllegalArgumentException() { + assertThatThrownBy(() -> NestedRecordUtils.reconstructCompositeKey("parent", null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Attribute name cannot be null"); + } + + @Test + public void reconstructCompositeKey_withSimplePath_returnsCompositeKey() { + String result = NestedRecordUtils.reconstructCompositeKey("parent", "attributeName"); + + assertThat(result).isEqualTo("parent" + NESTED_OBJECT_UPDATE + "attributeName"); + } + + @Test + public void reconstructCompositeKey_withDottedPath_returnsCompositeKey() { + String result = NestedRecordUtils.reconstructCompositeKey("parent.child", "attributeName"); + + assertThat(result).isEqualTo(String.join(NESTED_OBJECT_UPDATE, + "parent", "child", "attributeName")); + } + + @Test + public void reconstructCompositeKey_withDeepDottedPath_returnsCompositeKey() { + String result = NestedRecordUtils.reconstructCompositeKey("parent.child.grandchild", "attributeName"); + + assertThat(result).isEqualTo(String.join(NESTED_OBJECT_UPDATE, + "parent", "child", "grandchild", "attributeName")); + } + + @Test + public void reconstructCompositeKey_realWorldExample_userAddressZipCode() { + String result = NestedRecordUtils.reconstructCompositeKey("user.address.location", "zipCode"); + + String expected = String.join(NESTED_OBJECT_UPDATE, + "user", "address", "location", "zipCode"); + assertThat(result).isEqualTo(expected); + } +} \ No newline at end of file From 7bdc3aa6774e55667442b7607889eeab6f033c31 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 6 Feb 2026 12:55:08 +0200 Subject: [PATCH 15/18] Added support for @DynamoDbAutoGeneratedTimestampAttribute on attributes within nested objects --- .../AutoGeneratedTimestampExtensionTest.java | 158 +++++++++++++++++- .../internal/EnhancedClientUtilsTest.java | 139 +++++++++++++++ .../utility/NestedRecordUtilsTest.java | 65 ++++++- 3 files changed, 356 insertions(+), 6 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java index 798dd052a07a..5f32e88223d2 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java @@ -18,6 +18,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; @@ -32,9 +34,12 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import org.junit.After; @@ -52,6 +57,8 @@ import software.amazon.awssdk.enhanced.dynamodb.converters.TimeFormatUpdateTestConverter; import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithInvalidNestedAttributeName; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithInvalidRootAttributeName; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedBeanChild; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedBeanWithList; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedImmutableRecordWithList; @@ -63,8 +70,6 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithMap; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableRecordWithSet; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleStaticRecordWithList; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithInvalidNestedAttributeName; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithInvalidRootAttributeName; import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; @@ -772,6 +777,155 @@ public void autogenerateTimestamps_onNestedAttributeWithReservedMarker_throwsExc .build(); } + @Test + public void extension_create_returnsExtensionWithSystemClock() { + AutoGeneratedTimestampRecordExtension extension = AutoGeneratedTimestampRecordExtension.create(); + + assertThat(extension, is(notNullValue())); + } + + @Test + public void extension_builder_returnsBuilderInstance() { + AutoGeneratedTimestampRecordExtension.Builder builder = AutoGeneratedTimestampRecordExtension.builder(); + + assertThat(builder, is(notNullValue())); + } + + @Test + public void extension_builderWithCustomClock_usesCustomClock() { + Clock customClock = Clock.fixed(MOCKED_INSTANT_NOW, ZoneOffset.UTC); + AutoGeneratedTimestampRecordExtension extension = AutoGeneratedTimestampRecordExtension.builder() + .baseClock(customClock) + .build(); + + assertThat(extension, is(notNullValue())); + } + + @Test + public void extension_toBuilder_returnsBuilderWithExistingValues() { + Clock customClock = Clock.fixed(MOCKED_INSTANT_NOW, ZoneOffset.UTC); + AutoGeneratedTimestampRecordExtension extension = AutoGeneratedTimestampRecordExtension.builder() + .baseClock(customClock) + .build(); + + AutoGeneratedTimestampRecordExtension.Builder builder = extension.toBuilder(); + + assertThat(builder, is(notNullValue())); + assertThat(builder.build(), is(notNullValue())); + } + + @Test + public void attributeTags_autoGeneratedTimestampAttribute_returnsStaticAttributeTag() { + assertThat(autoGeneratedTimestampAttribute(), + is(notNullValue())); + } + + @Test + public void beforeWrite_withNullNestedObject_skipsProcessing() { + String tableName = getConcreteTableName("null-nested-table"); + DynamoDbTable table = enhancedClient.table(tableName, createNestedRecordSchema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Put item with null nested object + NestedRecord item = new NestedRecord() + .setId("id") + .setAttribute("test") + .setNestedRecord(null); // null nested object + + table.putItem(r -> r.item(item)); + NestedRecord result = table.getItem(r -> r.key(k -> k.partitionValue("id"))); + + // Root timestamps should be set, nested should be null + assertThat(result.getLastUpdatedDate(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getCreatedDate(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getNestedRecord(), is(nullValue())); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withEmptyListAttribute_skipsListProcessing() { + String tableName = getConcreteTableName("empty-list-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(SimpleBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Put item with empty list + SimpleBeanWithList item = new SimpleBeanWithList() + .setId("1") + .setChildList(Collections.emptyList()); // empty list + + table.putItem(r -> r.item(item)); + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Root timestamp should be set, list should be empty + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList(), hasSize(0)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withListContainingNullElements_handlesNullsGracefully() { + String tableName = getConcreteTableName("list-with-nulls-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(SimpleBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create list with null elements mixed with valid elements + List listWithNulls = new ArrayList<>(); + listWithNulls.add(null); + listWithNulls.add(new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1")); + listWithNulls.add(null); + listWithNulls.add(new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2")); + + SimpleBeanWithList item = new SimpleBeanWithList() + .setId("1") + .setChildList(listWithNulls); + + table.putItem(r -> r.item(item)); + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Root timestamp should be set + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList(), hasSize(4)); + + // Non-null elements should have timestamps + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(3).getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withDeepNestedStructure_updatesAllLevels() { + String tableName = getConcreteTableName("deep-nested-table"); + DynamoDbTable table = enhancedClient.table(tableName, createRecursiveRecordLevel1Schema()); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create deeply nested structure + RecursiveRecord level4 = new RecursiveRecord().setId("l4_id"); + RecursiveRecord level3 = new RecursiveRecord().setId("l3_id").setChild(level4); + RecursiveRecord level2 = new RecursiveRecord().setId("l2_id").setChild(level3); + RecursiveRecord level1 = new RecursiveRecord().setId("l1_id").setChild(level2); + + table.putItem(level1); + + GetItemResponse response = getItemFromDDB(table.tableName(), "l1_id"); + Map item = response.item(); + + // Verify all levels have timestamps + assertThat(item.get("parentTimestamp").s(), is(MOCKED_INSTANT_NOW.toString())); + + Map level2Map = item.get("child").m(); + assertThat(level2Map.get("childTimestamp").s(), is(MOCKED_INSTANT_NOW.toString())); + + Map level3Map = level2Map.get("child").m(); + assertThat(level3Map.get("grandchildTimestamp").s(), is(MOCKED_INSTANT_NOW.toString())); + + table.deleteTable(); + } + private GetItemResponse getItemFromDDB(String tableName, String id) { Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s(id).build()); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java index 5484eecc6f0a..f8d6cd2c2be7 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java @@ -222,4 +222,143 @@ public void getNestedSchema_withValidInputs_returnsNestedSchema() { assertThat(result).isPresent(); assertThat(result.get()).isEqualTo(mockNestedSchema); } + + @Test + public void keyRef_withSimpleKey_returnsFormattedKey() { + String result = EnhancedClientUtils.keyRef("simpleKey"); + + assertThat(result).isEqualTo("#AMZN_MAPPED_simpleKey"); + } + + @Test + public void keyRef_withSpecialCharacters_cleansAndFormatsKey() { + String result = EnhancedClientUtils.keyRef("key*with.special-chars"); + + assertThat(result).isEqualTo("#AMZN_MAPPED_key_with_special_chars"); + } + + @Test + public void keyRef_withNestedKey_handlesNestedDelimiter() { + String nestedKey = "parent_NESTED_ATTR_UPDATE_child"; + String result = EnhancedClientUtils.keyRef(nestedKey); + + assertThat(result).contains("#AMZN_MAPPED_"); + assertThat(result).contains("parent"); + assertThat(result).contains("child"); + } + + @Test + public void valueRef_withSimpleValue_returnsFormattedValue() { + String result = EnhancedClientUtils.valueRef("simpleValue"); + + assertThat(result).isEqualTo(":AMZN_MAPPED_simpleValue"); + } + + @Test + public void valueRef_withSpecialCharacters_cleansAndFormatsValue() { + String result = EnhancedClientUtils.valueRef("value*with.special-chars"); + + assertThat(result).isEqualTo(":AMZN_MAPPED_value_with_special_chars"); + } + + @Test + public void valueRef_withNestedValue_handlesNestedDelimiter() { + String nestedValue = "parent_NESTED_ATTR_UPDATE_child"; + String result = EnhancedClientUtils.valueRef(nestedValue); + + assertThat(result).startsWith(":AMZN_MAPPED_"); + assertThat(result).contains("parent"); + assertThat(result).contains("child"); + } + + @Test + public void cleanAttributeName_withNoSpecialCharacters_returnsOriginal() { + String original = "normalAttributeName123"; + String result = EnhancedClientUtils.cleanAttributeName(original); + + assertThat(result).isSameAs(original); // Should return same instance when no changes needed + } + + @Test + public void isNullAttributeValue_withNullAttributeValue_returnsTrue() { + AttributeValue nullValue = AttributeValue.builder().nul(true).build(); + + boolean result = EnhancedClientUtils.isNullAttributeValue(nullValue); + + assertThat(result).isTrue(); + } + + @Test + public void isNullAttributeValue_withNonNullAttributeValue_returnsFalse() { + AttributeValue stringValue = AttributeValue.builder().s("test").build(); + + boolean result = EnhancedClientUtils.isNullAttributeValue(stringValue); + + assertThat(result).isFalse(); + } + + @Test + public void isNullAttributeValue_withFalseNullValue_returnsFalse() { + AttributeValue falseNullValue = AttributeValue.builder().nul(false).build(); + + boolean result = EnhancedClientUtils.isNullAttributeValue(falseNullValue); + + assertThat(result).isFalse(); + } + + @Test + public void createKeyFromItem_withPartitionKeyOnly_createsCorrectKey() { + FakeItem item = new FakeItem(); + item.setId("test-id"); + + Key result = EnhancedClientUtils.createKeyFromItem(item, FakeItem.getTableSchema(), + TableMetadata.primaryIndexName()); + + assertThat(result.partitionKeyValue()).isEqualTo(AttributeValue.builder().s("test-id").build()); + assertThat(result.sortKeyValue()).isEmpty(); + } + + @Test + public void createKeyFromItem_withPartitionAndSortKey_createsCorrectKey() { + FakeItemWithSort item = new FakeItemWithSort(); + item.setId("test-id"); + item.setSort("test-sort"); + + Key result = EnhancedClientUtils.createKeyFromItem(item, FakeItemWithSort.getTableSchema(), + TableMetadata.primaryIndexName()); + + assertThat(result.partitionKeyValue()).isEqualTo(AttributeValue.builder().s("test-id").build()); + assertThat(result.sortKeyValue()).isPresent(); + assertThat(result.sortKeyValue().get()).isEqualTo(AttributeValue.builder().s("test-sort").build()); + } + + @Test + public void readAndTransformSingleItem_withNullItemMap_returnsNull() { + Object result = EnhancedClientUtils.readAndTransformSingleItem(null, mockSchema, null, null); + + assertThat(result).isNull(); + } + + @Test + public void readAndTransformSingleItem_withEmptyItemMap_returnsNull() { + Map emptyMap = Collections.emptyMap(); + + Object result = EnhancedClientUtils.readAndTransformSingleItem(emptyMap, mockSchema, null, null); + + assertThat(result).isNull(); + } + + @Test + public void getItemsFromSupplier_withNullList_returnsNull() { + List result = EnhancedClientUtils.getItemsFromSupplier(null); + + assertThat(result).isNull(); + } + + @Test + public void getItemsFromSupplier_withEmptyList_returnsNull() { + List result = EnhancedClientUtils.getItemsFromSupplier(Collections.emptyList()); + + assertThat(result).isNull(); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtilsTest.java index 9f8dbf9d831f..34bbc9a615e8 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtilsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/utility/NestedRecordUtilsTest.java @@ -17,11 +17,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.regex.Pattern; import org.junit.Test; import org.junit.runner.RunWith; @@ -231,11 +233,66 @@ public void reconstructCompositeKey_withDeepDottedPath_returnsCompositeKey() { } @Test - public void reconstructCompositeKey_realWorldExample_userAddressZipCode() { - String result = NestedRecordUtils.reconstructCompositeKey("user.address.location", "zipCode"); + public void getTableSchemaForListElement_withNestedPathAndMissingSchema_throwsIllegalArgumentException() { + String nestedKey = String.join(NESTED_OBJECT_UPDATE, "parent", "child", "listAttribute"); - String expected = String.join(NESTED_OBJECT_UPDATE, - "user", "address", "location", "zipCode"); + // Mock the parent schema resolution to return empty + when(mockSchema.converterForAttribute("parent")).thenReturn(null); + + assertThatThrownBy(() -> NestedRecordUtils.getTableSchemaForListElement(mockSchema, nestedKey)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unable to resolve schema for list element at: " + nestedKey); + } + + @Test + public void resolveSchemasPerPath_withDeepNestedPaths_buildsCorrectSchemaMap() { + Map deepNestedAttributes = new HashMap<>(); + deepNestedAttributes.put(String.join(NESTED_OBJECT_UPDATE, "level1", "level2", "level3", "attr"), + AttributeValue.builder().s("deep-value").build()); + + TableSchema level1Schema = mock(TableSchema.class); + TableSchema level2Schema = mock(TableSchema.class); + TableSchema level3Schema = mock(TableSchema.class); + + when(mockSchema.converterForAttribute("level1")).thenReturn(mockConverter); + when(mockConverter.type()).thenReturn(mockType); + when(mockType.tableSchema()).thenReturn(Optional.of(level1Schema)); + + when(mockConverter.type()).thenReturn(mockType); + when(mockType.tableSchema()).thenReturn(Optional.of(level2Schema)); + + when(mockConverter.type()).thenReturn(mockType); + when(mockType.tableSchema()).thenReturn(Optional.of(level3Schema)); + + Map> result = NestedRecordUtils.resolveSchemasPerPath(deepNestedAttributes, mockSchema); + + assertThat(result).containsKey(""); + assertThat(result.get("")).isEqualTo(mockSchema); + assertThat(result.size()).isGreaterThan(1); + } + + @Test + public void reconstructCompositeKey_withMultipleDots_handlesCorrectly() { + String result = NestedRecordUtils.reconstructCompositeKey("a.b.c.d.e", "finalAttribute"); + + String expected = String.join(NESTED_OBJECT_UPDATE, "a", "b", "c", "d", "e", "finalAttribute"); + assertThat(result).isEqualTo(expected); + } + + @Test + public void reconstructCompositeKey_withWhitespaceInPath_preservesWhitespace() { + String result = NestedRecordUtils.reconstructCompositeKey("parent with spaces.child with spaces", "attr"); + + String expected = String.join(NESTED_OBJECT_UPDATE, "parent with spaces", "child with spaces", "attr"); + assertThat(result).isEqualTo(expected); + } + + @Test + public void reconstructCompositeKey_withSpecialCharactersInPath_preservesCharacters() { + String result = NestedRecordUtils.reconstructCompositeKey( + "parent-with-dashes.child_with_underscores", "attr"); + + String expected = String.join(NESTED_OBJECT_UPDATE, "parent-with-dashes", "child_with_underscores", "attr"); assertThat(result).isEqualTo(expected); } } \ No newline at end of file From 7cef75e2ed8986294a9deaa57baed250107b5dcf Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 6 Feb 2026 13:27:48 +0200 Subject: [PATCH 16/18] Added support for @DynamoDbAutoGeneratedTimestampAttribute on attributes within nested objects --- .../AutoGeneratedTimestampExtensionTest.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java index 5f32e88223d2..e47d2625193b 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java @@ -1463,4 +1463,117 @@ private TableSchema createRecursiveRecordLevel1Schema() { .setter(RecursiveRecord::setChild)) .build(); } + + @Test + public void beforeWrite_withNestedListOfMaps_updatesTimestampsInListElements() { + String tableName = getConcreteTableName("nested-list-of-maps-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(SimpleBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create item with list of child objects (maps) + SimpleBeanWithList item = new SimpleBeanWithList() + .setId("1") + .setChildList(Arrays.asList( + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1"), + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2"), + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child3") + )); + + table.putItem(r -> r.item(item)); + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify timestamps at root and in all list elements + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList(), hasSize(3)); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(2).getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withNestedListOfNonMapElements_skipsListProcessing() { + String tableName = getConcreteTableName("nested-list-of-strings-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(SimpleBeanWithSet.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create item with set of strings (not maps) - should skip nested processing + SimpleBeanWithSet item = new SimpleBeanWithSet() + .setId("1") + .setChildSet(new HashSet<>(Arrays.asList("string1", "string2", "string3"))); + + table.putItem(r -> r.item(item)); + SimpleBeanWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify root timestamp is set, set elements are preserved + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildSet(), hasSize(3)); + assertThat(result.getChildSet().contains("string1"), is(true)); + assertThat(result.getChildSet().contains("string2"), is(true)); + assertThat(result.getChildSet().contains("string3"), is(true)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withMultipleNestedListUpdates_updatesAllTimestamps() { + String tableName = getConcreteTableName("multiple-nested-list-updates-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(SimpleBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Initial put + table.putItem(new SimpleBeanWithList() + .setId("1") + .setChildList(Arrays.asList( + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1"), + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2") + ))); + + // First update with new timestamp + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + table.updateItem(new SimpleBeanWithList() + .setId("1") + .setChildList(Arrays.asList( + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1_updated"), + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2_updated"), + new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child3_new") + ))); + + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify all timestamps are updated to the new time + assertThat(result.getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); + assertThat(result.getChildList(), hasSize(3)); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); + assertThat(result.getChildList().get(2).getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withEmptyNestedList_skipsNestedListProcessing() { + String tableName = getConcreteTableName("empty-nested-list-table"); + DynamoDbTable table = enhancedClient.table(tableName, + BeanTableSchema.create(SimpleBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create item with empty list + SimpleBeanWithList item = new SimpleBeanWithList() + .setId("1") + .setChildList(Collections.emptyList()); + + table.putItem(r -> r.item(item)); + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify root timestamp is set, empty list is preserved + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList(), hasSize(0)); + + table.deleteTable(); + } } \ No newline at end of file From d8bc88b160b03481055dcf658a541a5f7c5a82a4 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 6 Feb 2026 17:13:59 +0200 Subject: [PATCH 17/18] Added support for @DynamoDbAutoGeneratedTimestampAttribute on attributes within nested objects --- ...AutoGeneratedTimestampRecordExtension.java | 11 +- .../internal/EnhancedClientUtils.java | 4 + .../AutoGeneratedTimestampExtensionTest.java | 438 +++++++++++------- .../AutogeneratedTimestampTestModels.java | 217 +-------- .../internal/EnhancedClientUtilsTest.java | 35 ++ 5 files changed, 337 insertions(+), 368 deletions(-) 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 afc41280df6c..07794d67450e 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 @@ -16,6 +16,7 @@ package software.amazon.awssdk.enhanced.dynamodb.extensions; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.getNestedSchema; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.hasMap; import static software.amazon.awssdk.enhanced.dynamodb.internal.extensions.utility.NestedRecordUtils.getTableSchemaForListElement; import static software.amazon.awssdk.enhanced.dynamodb.internal.extensions.utility.NestedRecordUtils.reconstructCompositeKey; import static software.amazon.awssdk.enhanced.dynamodb.internal.extensions.utility.NestedRecordUtils.resolveSchemasPerPath; @@ -157,7 +158,7 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex Map, TableSchema> schemaInstanceCache = new HashMap<>(); itemToTransform.forEach((key, value) -> { - if (value.hasM() && value.m() != null) { + if (hasMap(value)) { Optional> nestedSchemaOpt = getNestedSchema(context.tableSchema(), key); if (nestedSchemaOpt.isPresent()) { TableSchema nestedSchema = nestedSchemaOpt.get(); @@ -173,13 +174,13 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex .findFirst() .orElse(null); - if (firstElement != null && firstElement.hasM()) { + if (hasMap(firstElement)) { TableSchema elementListSchema = getTableSchemaForListElement(context.tableSchema(), key); if (elementListSchema != null) { TableSchema cachedSchema = getOrCacheSchema(schemaInstanceCache, elementListSchema); Collection updatedList = new ArrayList<>(value.l().size()); for (AttributeValue listItem : value.l()) { - if (listItem != null && listItem.hasM()) { + if (hasMap(listItem)) { updatedList.add(AttributeValue.builder() .m(processNestedObject( listItem.m(), @@ -287,14 +288,14 @@ private Map processNestedObject(Map listElementSchema = getTableSchemaForListElement(nestedSchema, nestedKey); if (listElementSchema != null) { TableSchema cachedSchema = getOrCacheSchema(schemaInstanceCache, listElementSchema); Collection updatedList = new ArrayList<>(nestedValue.l().size()); boolean listModified = false; for (AttributeValue listItem : nestedValue.l()) { - if (listItem != null && listItem.hasM()) { + if (hasMap(listItem)) { AttributeValue updatedItem = AttributeValue.builder() .m(processNestedObject( listItem.m(), 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 fd7fde941d5a..81477aca1163 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 @@ -209,6 +209,10 @@ public static boolean isNullAttributeValue(AttributeValue attributeValue) { return attributeValue.nul() != null && attributeValue.nul(); } + public static boolean hasMap(AttributeValue attributeValue) { + return attributeValue != null && attributeValue.hasM(); + } + /** * Retrieves the nested {@link TableSchema} for an attribute from the parent schema. * For parameterized types (e.g., Set, List, Map), extracts the first type parameter's schema. diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java index e47d2625193b..1b9d5ef1b45e 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/AutoGeneratedTimestampExtensionTest.java @@ -23,6 +23,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension.AttributeTags.autoGeneratedTimestampAttribute; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleBeanChild; +import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleImmutableChild; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.buildStaticImmutableSchemaForNestedRecordWithList; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.buildStaticImmutableSchemaForSimpleRecordWithList; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.buildStaticSchemaForNestedRecordWithList; @@ -56,12 +58,14 @@ 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.functionaltests.models.AutogeneratedTimestampTestModels; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithInvalidNestedAttributeName; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.BeanWithInvalidRootAttributeName; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedBeanChild; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedBeanWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedImmutableChildRecordWithList; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedImmutableRecordWithList; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedStaticChildRecordWithList; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.NestedStaticRecordWithList; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleBeanWithList; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.AutogeneratedTimestampTestModels.SimpleBeanWithMap; @@ -438,16 +442,17 @@ public void recursiveRecord_allTimestampsAreUpdated() { @Test public void beanSchema_simpleRecordWithList_populatesTimestamps() { String tableName = getConcreteTableName("bean-simple-list-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(SimpleBeanWithList.class)); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(SimpleBeanWithList.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( new SimpleBeanWithList() .setId("1") .setChildList(Arrays.asList( - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1"), - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2")))); + new SimpleBeanChild().setId("child1"), + new SimpleBeanChild().setId("child2"))) + .setChildStringList(Collections.singletonList("test"))); SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); @@ -461,16 +466,16 @@ public void beanSchema_simpleRecordWithList_populatesTimestamps() { @Test public void beanSchema_simpleRecordWithSet_populatesTimestamps() { String tableName = getConcreteTableName("bean-simple-set-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(SimpleBeanWithSet.class)); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(SimpleBeanWithSet.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( - new AutogeneratedTimestampTestModels.SimpleBeanWithSet() + new SimpleBeanWithSet() .setId("1") .setChildSet(new HashSet<>(Arrays.asList("child1", "child2")))); - AutogeneratedTimestampTestModels.SimpleBeanWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + SimpleBeanWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); assertThat(result.getChildSet(), hasSize(2)); @@ -483,11 +488,11 @@ public void beanSchema_simpleRecordWithSet_populatesTimestamps() { @Test public void beanSchema_simpleRecordWithMap_populatesTimestamps() { String tableName = getConcreteTableName("bean-simple-map-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(AutogeneratedTimestampTestModels.SimpleBeanWithMap.class)); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(SimpleBeanWithMap.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( - new AutogeneratedTimestampTestModels.SimpleBeanWithMap() + new SimpleBeanWithMap() .setId("1") .setChildMap(new HashMap() {{ put("child1", "attr_child1"); @@ -507,8 +512,8 @@ public void beanSchema_simpleRecordWithMap_populatesTimestamps() { @Test public void beanSchema_nestedRecordWithList_populatesTimestamps() { String tableName = getConcreteTableName("bean-nested-list-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(NestedBeanWithList.class)); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(NestedBeanWithList.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( new NestedBeanWithList() @@ -526,21 +531,22 @@ public void beanSchema_nestedRecordWithList_populatesTimestamps() { @Test public void immutableSchema_simpleRecordWithList_populatesTimestamps() { String tableName = getConcreteTableName("immutable-simple-list-table"); - DynamoDbTable table = enhancedClient.table(tableName, - ImmutableTableSchema.create(SimpleImmutableRecordWithList.class)); + DynamoDbTable table = + enhancedClient.table(tableName, ImmutableTableSchema.create(SimpleImmutableRecordWithList.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( SimpleImmutableRecordWithList .builder() .id("1") .childList(Arrays.asList( - AutogeneratedTimestampTestModels.SimpleImmutableChild.builder().id("child1").build(), - AutogeneratedTimestampTestModels.SimpleImmutableChild.builder().id("child2").build())) + SimpleImmutableChild.builder().id("child1").build(), + SimpleImmutableChild.builder().id("child2").build())) .build()); SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().size(), is(2)); assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); @@ -550,18 +556,17 @@ public void immutableSchema_simpleRecordWithList_populatesTimestamps() { @Test public void immutableSchema_simpleRecordWithSet_populatesTimestamps() { String tableName = getConcreteTableName("immutable-simple-set-table"); - DynamoDbTable table = enhancedClient.table(tableName, - ImmutableTableSchema.create(SimpleImmutableRecordWithSet.class)); + DynamoDbTable table = + enhancedClient.table(tableName, ImmutableTableSchema.create(SimpleImmutableRecordWithSet.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( - AutogeneratedTimestampTestModels.SimpleImmutableRecordWithSet + SimpleImmutableRecordWithSet .builder() .id("1") .childSet(new HashSet<>(Arrays.asList("child1", "child2"))) .build()); - AutogeneratedTimestampTestModels.SimpleImmutableRecordWithSet result = table.getItem(r -> r.key(k -> k.partitionValue( - "1"))); + SimpleImmutableRecordWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); assertThat(result.getChildSet(), hasSize(2)); @@ -574,22 +579,22 @@ public void immutableSchema_simpleRecordWithSet_populatesTimestamps() { @Test public void immutableSchema_simpleRecordWithMap_populatesTimestamps() { String tableName = getConcreteTableName("immutable-simple-map-table"); - DynamoDbTable table = enhancedClient.table(tableName, - ImmutableTableSchema.create(SimpleImmutableRecordWithMap.class)); + DynamoDbTable table = + enhancedClient.table(tableName, ImmutableTableSchema.create(SimpleImmutableRecordWithMap.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - table.putItem( - AutogeneratedTimestampTestModels.SimpleImmutableRecordWithMap.builder() - .id("1") - .childMap(new HashMap() {{ - put("child1", "attr_child1"); - put("child2", "attr_child2"); - }}) - .build()); - - AutogeneratedTimestampTestModels.SimpleImmutableRecordWithMap result = table.getItem(r -> r.key(k -> k.partitionValue( + table.putItem(SimpleImmutableRecordWithMap.builder() + .id("1") + .childMap(new HashMap() {{ + put("child1", "attr_child1"); + put("child2", "attr_child2"); + }}) + .build()); + + SimpleImmutableRecordWithMap result = table.getItem(r -> r.key(k -> k.partitionValue( "1"))); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertNotNull(result.getChildMap()); assertThat(result.getChildMap().size(), is(2)); assertThat(result.getChildMap().get("child1"), is("attr_child1")); assertThat(result.getChildMap().get("child2"), is("attr_child2")); @@ -600,14 +605,14 @@ public void immutableSchema_simpleRecordWithMap_populatesTimestamps() { @Test public void immutableSchema_nestedRecordWithList_populatesTimestamps() { String tableName = getConcreteTableName("immutable-nested-list-table"); - DynamoDbTable table = enhancedClient.table(tableName, - ImmutableTableSchema.create(NestedImmutableRecordWithList.class)); + DynamoDbTable table = + enhancedClient.table(tableName, ImmutableTableSchema.create(NestedImmutableRecordWithList.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); table.putItem( NestedImmutableRecordWithList .builder() .id("1") - .level2(AutogeneratedTimestampTestModels.NestedImmutableChildRecordWithList.builder().build()) + .level2(NestedImmutableChildRecordWithList.builder().build()) .build() ); @@ -647,7 +652,7 @@ public void staticSchema_nestedRecordWithList_populatesTimestamps() { table.putItem( new NestedStaticRecordWithList() .setId("1") - .setLevel2(new AutogeneratedTimestampTestModels.NestedStaticChildRecordWithList())); + .setLevel2(new NestedStaticChildRecordWithList())); NestedStaticRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); @@ -668,13 +673,15 @@ public void staticImmutableSchema_simpleRecordWithList_populatesTimestamps() { .builder() .id("1") .childList(Arrays.asList( - AutogeneratedTimestampTestModels.SimpleImmutableChild.builder().id("child1").build(), - AutogeneratedTimestampTestModels.SimpleImmutableChild.builder().id("child2").build())) + SimpleImmutableChild.builder().id("child1").build(), + SimpleImmutableChild.builder().id("child2").build())) .build()); SimpleImmutableRecordWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertNotNull(result.getChildList()); + assertThat(result.getChildList().size(), is(2)); assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); @@ -691,7 +698,7 @@ public void staticImmutableSchema_nestedRecordWithList_populatesTimestamps() { NestedImmutableRecordWithList .builder() .id("1") - .level2(AutogeneratedTimestampTestModels.NestedImmutableChildRecordWithList.builder().build()) + .level2(NestedImmutableChildRecordWithList.builder().build()) .build()); NestedImmutableRecordWithList level1 = table.getItem(r -> r.key(k -> k.partitionValue("1"))); @@ -761,14 +768,14 @@ public void autogenerateTimestamps_onNestedAttributeWithReservedMarker_throwsExc .tags(primaryPartitionKey())) .addAttribute( EnhancedType.documentOf( - BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild.class, + BeanWithInvalidNestedAttributeNameChild.class, StaticTableSchema - .builder(BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild.class) - .newItemSupplier(BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild::new) + .builder(BeanWithInvalidNestedAttributeNameChild.class) + .newItemSupplier(BeanWithInvalidNestedAttributeNameChild::new) .addAttribute(Instant.class, a -> a.name("childAttr_NESTED_ATTR_UPDATE_") - .getter(BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild::getAttr_NESTED_ATTR_UPDATE_) - .setter(BeanWithInvalidNestedAttributeName.BeanWithInvalidNestedAttributeNameChild::setAttr_NESTED_ATTR_UPDATE_) + .getter(BeanWithInvalidNestedAttributeNameChild::getAttr_NESTED_ATTR_UPDATE_) + .setter(BeanWithInvalidNestedAttributeNameChild::setAttr_NESTED_ATTR_UPDATE_) .tags(autoGeneratedTimestampAttribute())) .build()), a -> a.name("nestedChildAttribute") @@ -846,8 +853,8 @@ public void beforeWrite_withNullNestedObject_skipsProcessing() { @Test public void beforeWrite_withEmptyListAttribute_skipsListProcessing() { String tableName = getConcreteTableName("empty-list-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(SimpleBeanWithList.class)); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(SimpleBeanWithList.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); // Put item with empty list @@ -868,16 +875,16 @@ public void beforeWrite_withEmptyListAttribute_skipsListProcessing() { @Test public void beforeWrite_withListContainingNullElements_handlesNullsGracefully() { String tableName = getConcreteTableName("list-with-nulls-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(SimpleBeanWithList.class)); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(SimpleBeanWithList.class)); table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); // Create list with null elements mixed with valid elements - List listWithNulls = new ArrayList<>(); + List listWithNulls = new ArrayList<>(); listWithNulls.add(null); - listWithNulls.add(new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1")); + listWithNulls.add(new SimpleBeanChild().setId("child1")); listWithNulls.add(null); - listWithNulls.add(new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2")); + listWithNulls.add(new SimpleBeanChild().setId("child2")); SimpleBeanWithList item = new SimpleBeanWithList() .setId("1") @@ -926,6 +933,215 @@ public void beforeWrite_withDeepNestedStructure_updatesAllLevels() { table.deleteTable(); } + @Test + public void beforeWrite_withNestedListOfMaps_updatesTimestampsInListElements() { + String tableName = getConcreteTableName("nested-list-of-maps-table"); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(SimpleBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create item with list of child objects (maps) + SimpleBeanWithList item = new SimpleBeanWithList() + .setId("1") + .setChildList(Arrays.asList( + new SimpleBeanChild().setId("child1"), + new SimpleBeanChild().setId("child2"), + new SimpleBeanChild().setId("child3") + )); + + table.putItem(r -> r.item(item)); + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify timestamps at root and in all list elements + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList(), hasSize(3)); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList().get(2).getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withNestedListOfNonMapElements_skipsListProcessing() { + String tableName = getConcreteTableName("nested-list-of-strings-table"); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(SimpleBeanWithSet.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create item with set of strings (not maps) - should skip nested processing + SimpleBeanWithSet item = new SimpleBeanWithSet() + .setId("1") + .setChildSet(new HashSet<>(Arrays.asList("string1", "string2", "string3"))); + + table.putItem(r -> r.item(item)); + SimpleBeanWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify root timestamp is set, set elements are preserved + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildSet(), hasSize(3)); + assertThat(result.getChildSet().contains("string1"), is(true)); + assertThat(result.getChildSet().contains("string2"), is(true)); + assertThat(result.getChildSet().contains("string3"), is(true)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withMultipleNestedListUpdates_updatesAllTimestamps() { + String tableName = getConcreteTableName("multiple-nested-list-updates-table"); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(SimpleBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Initial put + table.putItem(new SimpleBeanWithList() + .setId("1") + .setChildList(Arrays.asList( + new SimpleBeanChild().setId("child1"), + new SimpleBeanChild().setId("child2") + ))); + + // First update with new timestamp + Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); + table.updateItem(new SimpleBeanWithList() + .setId("1") + .setChildList(Arrays.asList( + new SimpleBeanChild().setId("child1_updated"), + new SimpleBeanChild().setId("child2_updated"), + new SimpleBeanChild().setId("child3_new") + ))); + + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify all timestamps are updated to the new time + assertThat(result.getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); + assertThat(result.getChildList(), hasSize(3)); + assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); + assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); + assertThat(result.getChildList().get(2).getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withEmptyNestedList_skipsNestedListProcessing() { + String tableName = getConcreteTableName("empty-nested-list-table"); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(SimpleBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create item with empty list + SimpleBeanWithList item = new SimpleBeanWithList() + .setId("1") + .setChildList(Collections.emptyList()); + + table.putItem(r -> r.item(item)); + SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify root timestamp is set, empty list is preserved + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getChildList(), hasSize(0)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withNestedObjectContainingListOfMaps_updatesTimestampsInNestedList() { + String tableName = getConcreteTableName("nested-object-with-list-table"); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(NestedBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create nested structure: root -> nested object -> list of child objects + // This tests the branch at lines 284-318 in AutoGeneratedTimestampRecordExtension + NestedBeanChild nestedChild = new NestedBeanChild(); + nestedChild.setChildList(Arrays.asList( + new SimpleBeanChild().setId("nested_child1"), + new SimpleBeanChild().setId("nested_child2"), + new SimpleBeanChild().setId("nested_child3") + )); + + NestedBeanWithList item = new NestedBeanWithList() + .setId("1") + .setLevel2(nestedChild); + + table.putItem(r -> r.item(item)); + NestedBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify timestamps at all levels: root, nested object, and nested list elements + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getChildList(), hasSize(3)); + assertThat(result.getLevel2().getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getChildList().get(2).getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withNestedObjectContainingListWithNulls_updatesTimestamps() { + String tableName = getConcreteTableName("nested-list-with-nulls-table"); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(NestedBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create nested structure with null elements in the list + List listWithNulls = new ArrayList<>(); + listWithNulls.add(new SimpleBeanChild().setId("child1")); + listWithNulls.add(null); + listWithNulls.add(new SimpleBeanChild().setId("child2")); + listWithNulls.add(null); + + NestedBeanChild nestedChild = new NestedBeanChild(); + nestedChild.setChildList(listWithNulls); + + NestedBeanWithList item = new NestedBeanWithList() + .setId("1") + .setLevel2(nestedChild); + + table.putItem(r -> r.item(item)); + NestedBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify timestamps are set for non-null elements, nulls are preserved + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getChildList(), hasSize(4)); + assertThat(result.getLevel2().getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getChildList().get(2).getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getChildList().get(3).getTime(), is(MOCKED_INSTANT_NOW)); + + table.deleteTable(); + } + + @Test + public void beforeWrite_withNestedObjectContainingEmptyList_skipsListProcessing() { + String tableName = getConcreteTableName("nested-empty-list-table"); + DynamoDbTable table = + enhancedClient.table(tableName, BeanTableSchema.create(NestedBeanWithList.class)); + table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + // Create nested structure with empty list + NestedBeanChild nestedChild = new NestedBeanChild(); + nestedChild.setChildList(Collections.emptyList()); + + NestedBeanWithList item = new NestedBeanWithList() + .setId("1") + .setLevel2(nestedChild); + + table.putItem(r -> r.item(item)); + NestedBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); + + // Verify timestamps are set at root and nested level, empty list is preserved + assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getTime(), is(MOCKED_INSTANT_NOW)); + assertThat(result.getLevel2().getChildList(), hasSize(0)); + + table.deleteTable(); + } + private GetItemResponse getItemFromDDB(String tableName, String id) { Map key = new HashMap<>(); key.put("id", AttributeValue.builder().s(id).build()); @@ -936,7 +1152,6 @@ private GetItemResponse getItemFromDDB(String tableName, String id) { .build()); } - /** * Basic record class for testing simple timestamp operations with multiple timestamp fields, different converters, and * flattened record structure. @@ -1463,117 +1678,4 @@ private TableSchema createRecursiveRecordLevel1Schema() { .setter(RecursiveRecord::setChild)) .build(); } - - @Test - public void beforeWrite_withNestedListOfMaps_updatesTimestampsInListElements() { - String tableName = getConcreteTableName("nested-list-of-maps-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(SimpleBeanWithList.class)); - table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - - // Create item with list of child objects (maps) - SimpleBeanWithList item = new SimpleBeanWithList() - .setId("1") - .setChildList(Arrays.asList( - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1"), - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2"), - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child3") - )); - - table.putItem(r -> r.item(item)); - SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - - // Verify timestamps at root and in all list elements - assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList(), hasSize(3)); - assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList().get(2).getTime(), is(MOCKED_INSTANT_NOW)); - - table.deleteTable(); - } - - @Test - public void beforeWrite_withNestedListOfNonMapElements_skipsListProcessing() { - String tableName = getConcreteTableName("nested-list-of-strings-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(SimpleBeanWithSet.class)); - table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - - // Create item with set of strings (not maps) - should skip nested processing - SimpleBeanWithSet item = new SimpleBeanWithSet() - .setId("1") - .setChildSet(new HashSet<>(Arrays.asList("string1", "string2", "string3"))); - - table.putItem(r -> r.item(item)); - SimpleBeanWithSet result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - - // Verify root timestamp is set, set elements are preserved - assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildSet(), hasSize(3)); - assertThat(result.getChildSet().contains("string1"), is(true)); - assertThat(result.getChildSet().contains("string2"), is(true)); - assertThat(result.getChildSet().contains("string3"), is(true)); - - table.deleteTable(); - } - - @Test - public void beforeWrite_withMultipleNestedListUpdates_updatesAllTimestamps() { - String tableName = getConcreteTableName("multiple-nested-list-updates-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(SimpleBeanWithList.class)); - table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - - // Initial put - table.putItem(new SimpleBeanWithList() - .setId("1") - .setChildList(Arrays.asList( - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1"), - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2") - ))); - - // First update with new timestamp - Mockito.when(mockClock.instant()).thenReturn(MOCKED_INSTANT_UPDATE_ONE); - table.updateItem(new SimpleBeanWithList() - .setId("1") - .setChildList(Arrays.asList( - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child1_updated"), - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child2_updated"), - new AutogeneratedTimestampTestModels.SimpleBeanChild().setId("child3_new") - ))); - - SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - - // Verify all timestamps are updated to the new time - assertThat(result.getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); - assertThat(result.getChildList(), hasSize(3)); - assertThat(result.getChildList().get(0).getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); - assertThat(result.getChildList().get(1).getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); - assertThat(result.getChildList().get(2).getTime(), is(MOCKED_INSTANT_UPDATE_ONE)); - - table.deleteTable(); - } - - @Test - public void beforeWrite_withEmptyNestedList_skipsNestedListProcessing() { - String tableName = getConcreteTableName("empty-nested-list-table"); - DynamoDbTable table = enhancedClient.table(tableName, - BeanTableSchema.create(SimpleBeanWithList.class)); - table.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - - // Create item with empty list - SimpleBeanWithList item = new SimpleBeanWithList() - .setId("1") - .setChildList(Collections.emptyList()); - - table.putItem(r -> r.item(item)); - SimpleBeanWithList result = table.getItem(r -> r.key(k -> k.partitionValue("1"))); - - // Verify root timestamp is set, empty list is preserved - assertThat(result.getTime(), is(MOCKED_INSTANT_NOW)); - assertThat(result.getChildList(), hasSize(0)); - - table.deleteTable(); - } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java index 83cdcc6af600..527fc44c6695 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/AutogeneratedTimestampTestModels.java @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; @@ -47,6 +46,7 @@ public static class SimpleBeanWithList { private String id; private Instant time; private List childList; + private List childStringList; @DynamoDbPartitionKey public String getId() { @@ -73,26 +73,17 @@ public List getChildList() { } public SimpleBeanWithList setChildList(List childList) { - this.childList = childList; + this.childList = Collections.unmodifiableList(childList); return this; } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleBeanWithList that = (SimpleBeanWithList) o; - return Objects.equals(id, that.id) && - Objects.equals(time, that.time) && Objects.equals(childList, that.childList); + public List getChildStringList() { + return childStringList; } - @Override - public int hashCode() { - return Objects.hash(id, time, childList); + public SimpleBeanWithList setChildStringList(List childStringList) { + this.childStringList = childStringList; + return this; } } @@ -127,7 +118,7 @@ public Set getChildSet() { } public SimpleBeanWithSet setChildSet(Set childSet) { - this.childSet = childSet; + this.childSet = Collections.unmodifiableSet(childSet); return this; } } @@ -163,7 +154,7 @@ public Map getChildMap() { } public SimpleBeanWithMap setChildMap(Map childMap) { - this.childMap = childMap; + this.childMap = Collections.unmodifiableMap(childMap); return this; } } @@ -192,23 +183,6 @@ public SimpleBeanChild setTime(Instant time) { this.time = time; return this; } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleBeanChild that = (SimpleBeanChild) o; - return Objects.equals(id, that.id) && Objects.equals(time, that.time); - } - - @Override - public int hashCode() { - return Objects.hash(id, time); - } } @DynamoDbBean @@ -245,29 +219,12 @@ public NestedBeanWithList setLevel2(NestedBeanChild level2) { this.level2 = level2; return this; } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedBeanWithList that = (NestedBeanWithList) o; - return Objects.equals(id, that.id) && - Objects.equals(time, that.time) && Objects.equals(level2, that.level2); - } - - @Override - public int hashCode() { - return Objects.hash(id, time, level2); - } } @DynamoDbBean public static class NestedBeanChild { private Instant time; + private List childList; @DynamoDbAutoGeneratedTimestampAttribute public Instant getTime() { @@ -279,21 +236,13 @@ public NestedBeanChild setTime(Instant time) { return this; } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedBeanChild that = (NestedBeanChild) o; - return Objects.equals(time, that.time); + public List getChildList() { + return childList == null ? null : Collections.unmodifiableList(childList); } - @Override - public int hashCode() { - return Objects.hash(time); + public NestedBeanChild setChildList(List childList) { + this.childList = Collections.unmodifiableList(childList); + return this; } } @@ -343,7 +292,7 @@ public Builder time(Instant time) { } public Builder childList(List childList) { - this.childList = childList; + this.childList = Collections.unmodifiableList(childList); return this; } @@ -351,24 +300,6 @@ public SimpleImmutableRecordWithList build() { return new SimpleImmutableRecordWithList(this); } } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleImmutableRecordWithList that = (SimpleImmutableRecordWithList) o; - return Objects.equals(id, that.id) && - Objects.equals(time, that.time) && Objects.equals(childList, that.childList); - } - - @Override - public int hashCode() { - return Objects.hash(id, time, childList); - } } @DynamoDbImmutable(builder = SimpleImmutableChild.Builder.class) @@ -413,23 +344,6 @@ public SimpleImmutableChild build() { return new SimpleImmutableChild(this); } } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleImmutableChild that = (SimpleImmutableChild) o; - return Objects.equals(id, that.id) && Objects.equals(time, that.time); - } - - @Override - public int hashCode() { - return Objects.hash(id, time); - } } @DynamoDbImmutable(builder = SimpleImmutableRecordWithSet.Builder.class) @@ -478,7 +392,7 @@ public Builder time(Instant time) { } public Builder childSet(Set childSet) { - this.childSet = childSet; + this.childSet = Collections.unmodifiableSet(childSet); return this; } @@ -534,7 +448,7 @@ public Builder time(Instant time) { } public Builder childMap(Map childMap) { - this.childMap = childMap; + this.childMap = Collections.unmodifiableMap(childMap); return this; } @@ -598,24 +512,6 @@ public NestedImmutableRecordWithList build() { return new NestedImmutableRecordWithList(this); } } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedImmutableRecordWithList that = (NestedImmutableRecordWithList) o; - return Objects.equals(id, that.id) && - Objects.equals(time, that.time) && Objects.equals(level2, that.level2); - } - - @Override - public int hashCode() { - return Objects.hash(id, time, level2); - } } @DynamoDbImmutable(builder = NestedImmutableChildRecordWithList.Builder.class) @@ -647,23 +543,6 @@ public NestedImmutableChildRecordWithList build() { return new NestedImmutableChildRecordWithList(this); } } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedImmutableChildRecordWithList that = (NestedImmutableChildRecordWithList) o; - return Objects.equals(time, that.time); - } - - @Override - public int hashCode() { - return Objects.hash(time); - } } public static class SimpleStaticRecordWithList { @@ -687,23 +566,6 @@ public SimpleStaticRecordWithList setTime(Instant time) { this.time = time; return this; } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SimpleStaticRecordWithList that = (SimpleStaticRecordWithList) o; - return Objects.equals(id, that.id) && Objects.equals(time, that.time); - } - - @Override - public int hashCode() { - return Objects.hash(id, time); - } } public static class NestedStaticRecordWithList { @@ -737,24 +599,6 @@ public NestedStaticRecordWithList setLevel2(NestedStaticChildRecordWithList leve this.level2 = level2; return this; } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedStaticRecordWithList that = (NestedStaticRecordWithList) o; - return Objects.equals(id, that.id) && - Objects.equals(time, that.time) && Objects.equals(level2, that.level2); - } - - @Override - public int hashCode() { - return Objects.hash(id, time, level2); - } } public static class NestedStaticChildRecordWithList { @@ -768,23 +612,6 @@ public NestedStaticChildRecordWithList setTime(Instant time) { this.time = time; return this; } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NestedStaticChildRecordWithList that = (NestedStaticChildRecordWithList) o; - return Objects.equals(time, that.time); - } - - @Override - public int hashCode() { - return Objects.hash(time); - } } public static TableSchema buildStaticSchemaForSimpleRecordWithList() { @@ -897,8 +724,8 @@ public static TableSchema buildStaticImmutableSch } /** - * Test model with an invalid root-level attribute name containing the reserved '_NESTED_ATTR_UPDATE_' pattern. - * Used to test validation of attribute names that conflict with internal DynamoDB Enhanced Client conventions. + * Test model with an invalid root-level attribute name containing the reserved '_NESTED_ATTR_UPDATE_' pattern. Used to test + * validation of attribute names that conflict with internal DynamoDB Enhanced Client conventions. */ @DynamoDbBean public static class BeanWithInvalidRootAttributeName { @@ -927,8 +754,8 @@ public BeanWithInvalidRootAttributeName setAttr_NESTED_ATTR_UPDATE_(Instant attr } /** - * Test model with an invalid nested attribute name containing the reserved '_NESTED_ATTR_UPDATE_' pattern. - * Used to test validation of nested attribute names that conflict with internal DynamoDB Enhanced Client conventions. + * Test model with an invalid nested attribute name containing the reserved '_NESTED_ATTR_UPDATE_' pattern. Used to test + * validation of nested attribute names that conflict with internal DynamoDB Enhanced Client conventions. */ @DynamoDbBean public static class BeanWithInvalidNestedAttributeName { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java index f8d6cd2c2be7..269b4e985b1b 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java @@ -57,6 +57,41 @@ public class EnhancedClientUtilsTest { @Mock private TableSchema mockNestedSchema; + @Test + public void hasMap_returnsTrue() { + AttributeValue nullValue = AttributeValue.builder().nul(false).m(new HashMap<>()).build(); + + boolean result = EnhancedClientUtils.hasMap(nullValue); + + assertThat(result).isTrue(); + } + + @Test + public void hasMap_forNullAttributeValue_returnsFalse() { + AttributeValue nullValue = AttributeValue.builder().nul(true).build(); + + boolean result = EnhancedClientUtils.hasMap(nullValue); + + assertThat(result).isFalse(); + } + + @Test + public void hasMap_forNotNullAttributeValueWithoutMap_returnsFalse() { + AttributeValue nullValue = AttributeValue.builder().nul(false).build(); + + boolean result = EnhancedClientUtils.hasMap(nullValue); + + assertThat(result).isFalse(); + } + + @Test + public void hasMap_forAttributeValueNull_returnsFalse() { + + boolean result = EnhancedClientUtils.hasMap(null); + + assertThat(result).isFalse(); + } + @Test public void createKeyFromMap_partitionOnly() { Map itemMap = new HashMap<>(); From ba342bf1fda8cd932b7ecd066b3f7d322b185980 Mon Sep 17 00:00:00 2001 From: Ana Satirbasa Date: Fri, 6 Feb 2026 17:14:38 +0200 Subject: [PATCH 18/18] Added support for @DynamoDbAutoGeneratedTimestampAttribute on attributes within nested objects --- .../enhanced/dynamodb/internal/EnhancedClientUtilsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java index 269b4e985b1b..30a6e142eedf 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtilsTest.java @@ -58,7 +58,7 @@ public class EnhancedClientUtilsTest { private TableSchema mockNestedSchema; @Test - public void hasMap_returnsTrue() { + public void hasMap_forNotNullAttributeValueWithMap_returnsTrue() { AttributeValue nullValue = AttributeValue.builder().nul(false).m(new HashMap<>()).build(); boolean result = EnhancedClientUtils.hasMap(nullValue);