diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/NestedAttributeName.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/NestedAttributeName.java new file mode 100644 index 000000000000..21404c4b47c8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/NestedAttributeName.java @@ -0,0 +1,235 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.Validate; + +/** + * High-level representation of a DynamoDB 'NestedAttributeName' that can be used in various situations where the API requires + * or accepts an Nested Attribute Name. + * Simple Attribute Name can be represented by passing just the name of the attribute. + * Nested Attributes are represented by List of String where each index of list corresponds to Nesting level Names. + *

While using attributeToProject in {@link software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest} + * and {@link software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest} we need way to represent Nested Attributes. + * The normal DOT(.) separator is not recognized as a Nesting level separator by DynamoDB request, + * thus we need to use NestedAttributeName + * which can be used to represent Nested attributes. + *

Example : NestedAttributeName.create("foo") corresponds to a NestedAttributeName with elements list + * with single element foo which represents Simple attribute name "foo" without nesting. + *

NestedAttributeName.create("foo", "bar") corresponds to a NestedAttributeName with elements list "foo", "bar" + * respresenting nested attribute name "foo.bar". + */ +@SdkPublicApi +public final class NestedAttributeName { + + private final List elements; + + private NestedAttributeName(List nestedAttributeNames) { + Validate.validState(nestedAttributeNames != null, "nestedAttributeNames must not be null."); + Validate.notEmpty(nestedAttributeNames, "nestedAttributeNames must not be empty"); + Validate.noNullElements(nestedAttributeNames, "nestedAttributeNames must not contain null values"); + this.elements = Collections.unmodifiableList(nestedAttributeNames); + } + + /** + * Creates a NestedAttributeName with a single element, which is effectively just a simple attribute name without nesting. + *

+ * Example:create("foo") will create NestedAttributeName corresponding to Attribute foo. + * + * @param element Attribute Name. Single String represents just a simple attribute name without nesting. + * @return NestedAttributeName with attribute name as specified element. + */ + public static NestedAttributeName create(String element) { + return new Builder().addElement(element).build(); + } + + /** + * Creates a NestedAttributeName from a list of elements that compose the full path of the nested attribute. + *

+ * Example:create("foo", "bar") will create NestedAttributeName which represents foo.bar nested attribute. + * + * @param elements Nested Attribute Names. Each of strings in varargs represent the nested attribute name + * at subsequent levels. + * @return NestedAttributeName with Nested attribute name set as specified in elements var args. + */ + public static NestedAttributeName create(String... elements) { + return new Builder().elements(elements).build(); + } + + /** + * Creates a NestedAttributeName from a list of elements that compose the full path of the nested attribute. + *

+ * Example:create(Arrays.asList("foo", "bar")) will create NestedAttributeName + * which represents foo.bar nested attribute. + * + * @param elements List of Nested Attribute Names. Each of strings in List represent the nested attribute name + * at subsequent levels. + * @return NestedAttributeName with Nested attribute name set as specified in elements Collections. + */ + public static NestedAttributeName create(List elements) { + return new Builder().elements(elements).build(); + } + + /** + * Create a builder that can be used to create a {@link NestedAttributeName}. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets elements of NestedAttributeName in the form of List. Each element in the list corresponds + * to the subsequent Nested Attribute name. + * + * @return List of nested attributes, each entry in the list represent one level of nesting. + * Example, A Two level Attribute name foo.bar will be represented as ["foo", "bar"] + */ + public List elements() { + return elements; + } + + /** + * Returns a builder initialized with all existing values on the request object. + */ + public Builder toBuilder() { + return builder().elements(elements); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedAttributeName that = (NestedAttributeName) o; + + return elements != null + ? elements.equals(that.elements) : that.elements == null; + } + + @Override + public int hashCode() { + return elements != null ? elements.hashCode() : 0; + } + + /** + * A builder for {@link NestedAttributeName}. + */ + public static class Builder { + private List elements = null; + + private Builder() { + + } + + /** + * Adds a single element of NestedAttributeName. + * Subsequent calls to this method can add attribute Names at subsequent nesting levels. + *

+ * Example:builder().addElement("foo").addElement("bar") will add elements in NestedAttributeName + * which represent a Nested Attribute Name foo.bar + * + * @param element Attribute Name. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder addElement(String element) { + if (elements == null) { + elements = new ArrayList<>(); + } + elements.add(element); + return this; + } + + /** + * Adds a single element of NestedAttributeName. + * Subsequent calls to this method will append the new elements to the end of the existing chain of elements + * creating new levels of nesting. + *

+ * Example:builder().addElements("foo","bar") will add elements in NestedAttributeName + * which represent a Nested Attribute Name foo.bar + * + * @param elements Nested Attribute Names. Each of strings in varargs represent the nested attribute name + * at subsequent levels. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder addElements(String... elements) { + if (this.elements == null) { + this.elements = new ArrayList<>(); + } + this.elements.addAll(Arrays.asList(elements)); + return this; + } + + /** + * Adds a List of elements to NestedAttributeName. + * Subsequent calls to this method will append the new elements to the end of the existing chain of elements + * creating new levels of nesting. + *

+ * Example:builder().addElements(Arrays.asList("foo","bar")) will add elements in NestedAttributeName + * to represent a Nested Attribute Name foo.bar + * + * @param elements List of Strings where each string corresponds to subsequent nesting attribute name. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder addElements(List elements) { + if (this.elements == null) { + this.elements = new ArrayList<>(); + } + this.elements.addAll(elements); + return this; + } + + /** + * Set elements of NestedAttributeName with list of Strings. Will overwrite any existing elements stored by this builder. + *

+ * Example:builder().elements("foo","bar") will set the elements in NestedAttributeName + * to represent a nested attribute name of 'foo.bar' + * + * @param elements a list of strings that correspond to the elements in a nested attribute name. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder elements(String... elements) { + this.elements = new ArrayList<>(Arrays.asList(elements)); + return this; + } + + /** + * Sets the elements that compose a nested attribute name. Will overwrite any existing elements stored by this builder. + *

+ * Example:builder().elements(Arrays.asList("foo","bar")) will add elements in NestedAttributeName + * which represent a Nested Attribute Name foo.bar + * + * @param elements a list of strings that correspond to the elements in a nested attribute name. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder elements(List elements) { + this.elements = new ArrayList<>(elements); + return this; + } + + + public NestedAttributeName build() { + return new NestedAttributeName(elements); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/ProjectionExpressionConvertor.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/ProjectionExpressionConvertor.java new file mode 100644 index 000000000000..f5d03ac9fdd7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/ProjectionExpressionConvertor.java @@ -0,0 +1,107 @@ +/* + * 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; + +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.cleanAttributeName; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; + +/** + * Wrapper method to get Projection Expression Name map and Projection Expressions from NestedAttributeNames. + */ +@SdkInternalApi +public class ProjectionExpressionConvertor { + + private static final String AMZN_MAPPED = "#AMZN_MAPPED_"; + private static final UnaryOperator PROJECTION_EXPRESSION_KEY_MAPPER = k -> AMZN_MAPPED + cleanAttributeName(k); + private final List nestedAttributeNames; + + private ProjectionExpressionConvertor(List nestedAttributeNames) { + this.nestedAttributeNames = nestedAttributeNames; + } + + public static ProjectionExpressionConvertor create(List nestedAttributeNames) { + return new ProjectionExpressionConvertor(nestedAttributeNames); + } + + private static Optional> convertToExpressionNameMap(NestedAttributeName attributeName) { + List nestedAttributeNames = attributeName.elements(); + if (nestedAttributeNames != null) { + Map resultNameMap = new LinkedHashMap<>(); + nestedAttributeNames.stream().forEach(nestedAttribute -> + resultNameMap.put(PROJECTION_EXPRESSION_KEY_MAPPER.apply(nestedAttribute), nestedAttribute)); + return Optional.of(resultNameMap); + } + return Optional.empty(); + } + + private static Optional convertToNameExpression(NestedAttributeName nestedAttributeName) { + + String name = nestedAttributeName.elements().stream().findFirst().orElse(null); + + List nestedAttributes = null; + if (nestedAttributeName.elements().size() > 1) { + nestedAttributes = nestedAttributeName.elements().subList(1, nestedAttributeName.elements().size()); + } + if (name != null) { + List hashSeparatedNestedStringList = + new ArrayList<>(Arrays.asList(PROJECTION_EXPRESSION_KEY_MAPPER.apply(name))); + if (nestedAttributes != null) { + nestedAttributes.stream().forEach(hashSeparatedNestedStringList::add); + } + return Optional.of(String.join(".".concat(AMZN_MAPPED), hashSeparatedNestedStringList)); + } + return Optional.empty(); + } + + public List nestedAttributeNames() { + return nestedAttributeNames; + } + + public Map convertToExpressionMap() { + Map attributeNameMap = new LinkedHashMap<>(); + if (this.nestedAttributeNames() != null) { + this.nestedAttributeNames().stream().forEach(attribs -> convertToExpressionNameMap(attribs) + .ifPresent(attributeNameMap::putAll)); + } + return attributeNameMap; + } + + public Optional convertToProjectionExpression() { + if (nestedAttributeNames != null) { + List expressionList = new ArrayList<>(); + this.nestedAttributeNames().stream().filter(Objects::nonNull) + .filter(item -> item.elements() != null && !item.elements().isEmpty()) + .forEach(attributeName -> convertToNameExpression(attributeName) + .ifPresent(expressionList::add)); + String joinedExpression = String.join(",", expressionList.stream() + .distinct().collect(Collectors.toList())); + return Optional.ofNullable(joinedExpression.isEmpty() ? null : joinedExpression); + } + return Optional.empty(); + } + +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperation.java index 5f05d1bfa726..01a98fb9bb7e 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperation.java @@ -15,14 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; -import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.cleanAttributeName; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.function.Function; -import java.util.function.UnaryOperator; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.async.SdkPublisher; import software.amazon.awssdk.core.pagination.sync.SdkIterable; @@ -32,6 +26,7 @@ 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.ProjectionExpressionConvertor; import software.amazon.awssdk.enhanced.dynamodb.model.Page; import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; @@ -42,9 +37,7 @@ @SdkInternalApi public class QueryOperation implements PaginatedTableOperation, - PaginatedIndexOperation { - - private static final UnaryOperator PROJECTION_EXPRESSION_KEY_MAPPER = k -> "#AMZN_MAPPED_" + cleanAttributeName(k); + PaginatedIndexOperation { private final QueryEnhancedRequest request; @@ -69,18 +62,13 @@ public QueryRequest generateRequest(TableSchema tableSchema, expressionNames = Expression.joinNames(expressionNames, this.request.filterExpression().expressionNames()); } - String projectionExpression = null; - if (this.request.attributesToProject() != null) { - List placeholders = new ArrayList<>(); - Map projectionPlaceholders = new HashMap<>(); - this.request.attributesToProject().forEach(attr -> { - String placeholder = PROJECTION_EXPRESSION_KEY_MAPPER.apply(attr); - placeholders.add(placeholder); - projectionPlaceholders.put(placeholder, attr); - }); - projectionExpression = String.join(",", placeholders); - expressionNames = Expression.joinNames(expressionNames, projectionPlaceholders); + ProjectionExpressionConvertor attributeToProject = + ProjectionExpressionConvertor.create(this.request.nestedAttributesToProject()); + Map projectionNameMap = attributeToProject.convertToExpressionMap(); + if (!projectionNameMap.isEmpty()) { + expressionNames = Expression.joinNames(expressionNames, projectionNameMap); } + String projectionExpression = attributeToProject.convertToProjectionExpression().orElse(null); QueryRequest.Builder queryRequest = QueryRequest.builder() .tableName(operationContext.tableName()) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/ScanOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/ScanOperation.java index 1b999eb5b8f6..9d6243290cc7 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/ScanOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/ScanOperation.java @@ -15,14 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; -import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.cleanAttributeName; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.function.Function; -import java.util.function.UnaryOperator; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.async.SdkPublisher; import software.amazon.awssdk.core.pagination.sync.SdkIterable; @@ -32,6 +26,7 @@ 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.ProjectionExpressionConvertor; import software.amazon.awssdk.enhanced.dynamodb.model.Page; import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; @@ -42,9 +37,8 @@ @SdkInternalApi public class ScanOperation implements PaginatedTableOperation, - PaginatedIndexOperation { + PaginatedIndexOperation { - private static final UnaryOperator PROJECTION_EXPRESSION_KEY_MAPPER = k -> "#AMZN_MAPPED_" + cleanAttributeName(k); private final ScanEnhancedRequest request; @@ -68,18 +62,13 @@ public ScanRequest generateRequest(TableSchema tableSchema, expressionNames = this.request.filterExpression().expressionNames(); } - String projectionExpression = null; - if (this.request.attributesToProject() != null) { - List placeholders = new ArrayList<>(); - Map projectionPlaceholders = new HashMap<>(); - this.request.attributesToProject().forEach(attr -> { - String placeholder = PROJECTION_EXPRESSION_KEY_MAPPER.apply(attr); - placeholders.add(placeholder); - projectionPlaceholders.put(placeholder, attr); - }); - projectionExpression = String.join(",", placeholders); - expressionNames = Expression.joinNames(expressionNames, projectionPlaceholders); + ProjectionExpressionConvertor attributeToProject = + ProjectionExpressionConvertor.create(this.request.nestedAttributesToProject()); + Map projectionNameMap = attributeToProject.convertToExpressionMap(); + if (!projectionNameMap.isEmpty()) { + expressionNames = Expression.joinNames(expressionNames, projectionNameMap); } + String projectionExpression = attributeToProject.convertToProjectionExpression().orElse(null); ScanRequest.Builder scanRequest = ScanRequest.builder() .tableName(operationContext.tableName()) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryEnhancedRequest.java index f944e770639c..d1727f6e9122 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryEnhancedRequest.java @@ -22,11 +22,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncIndex; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.Validate; /** * Defines parameters used to when querying a DynamoDb table or index using the query() operation (such as @@ -46,7 +49,7 @@ public final class QueryEnhancedRequest { private final Integer limit; private final Boolean consistentRead; private final Expression filterExpression; - private final List attributesToProject; + private final List attributesToProject; private QueryEnhancedRequest(Builder builder) { this.queryConditional = builder.queryConditional; @@ -72,12 +75,12 @@ public static Builder builder() { */ public Builder toBuilder() { return builder().queryConditional(queryConditional) - .exclusiveStartKey(exclusiveStartKey) - .scanIndexForward(scanIndexForward) - .limit(limit) - .consistentRead(consistentRead) - .filterExpression(filterExpression) - .attributesToProject(attributesToProject); + .exclusiveStartKey(exclusiveStartKey) + .scanIndexForward(scanIndexForward) + .limit(limit) + .consistentRead(consistentRead) + .filterExpression(filterExpression) + .addNestedAttributesToProject(attributesToProject); } /** @@ -125,8 +128,23 @@ public Expression filterExpression() { /** * Returns the list of projected attributes on this request object, or an null if no projection is specified. + * This is the single list which has Nested and Non Nested attributes to project. + * The Nested Attributes are represented using DOT separator in this List. + * Example : foo.bar is represented as "foo.bar" which is indistinguishable from a non-nested attribute + * with the name "foo.bar". + * Use {@link #nestedAttributesToProject} if you have a use-case that requires discrimination between these two cases. */ public List attributesToProject() { + return attributesToProject != null ? attributesToProject.stream() + .map(item -> String.join(".", item.elements())).collect(Collectors.toList()) : null; + } + + /** + * Returns the list of projected attribute names, in the form of {@link NestedAttributeName} objects, + * for this request object, or null if no projection is specified. + * Refer {@link NestedAttributeName} . + */ + public List nestedAttributesToProject() { return attributesToProject; } @@ -160,9 +178,7 @@ public boolean equals(Object o) { return false; } if (attributesToProject != null - ? ! attributesToProject.equals(query.attributesToProject) - : query.attributesToProject != null - ) { + ? !attributesToProject.equals(query.attributesToProject) : query.attributesToProject != null) { return false; } return filterExpression != null ? filterExpression.equals(query.filterExpression) : query.filterExpression == null; @@ -192,7 +208,7 @@ public static final class Builder { private Integer limit; private Boolean consistentRead; private Expression filterExpression; - private List attributesToProject; + private List attributesToProject; private Builder() { } @@ -288,18 +304,25 @@ public Builder filterExpression(Expression filterExpression) { *

* If no attribute names are specified, then all attributes will be returned. If any of the requested attributes * are not found, they will not appear in the result. + * If there are nested attributes then addNestedAttributesToProject API should be used. *

*

* For more information, see Accessing Item Attributes in the Amazon DynamoDB Developer Guide. *

- * @param attributesToProject - * A collection of the attributes names to be retrieved from the database. + * + * @param attributesToProject A collection of the attributes names to be retrieved from the database. * @return Returns a reference to this object so that method calls can be chained together. */ public Builder attributesToProject(Collection attributesToProject) { - this.attributesToProject = attributesToProject != null ? new ArrayList<>(attributesToProject) : null; + if (this.attributesToProject != null) { + this.attributesToProject.clear(); + } + if (attributesToProject != null) { + addNestedAttributesToProject(new ArrayList<>(attributesToProject).stream() + .map(NestedAttributeName::create).collect(Collectors.toList())); + } return this; } @@ -311,14 +334,15 @@ public Builder attributesToProject(Collection attributesToProject) { *

* If no attribute names are specified, then all attributes will be returned. If any of the requested attributes * are not found, they will not appear in the result. + * If there are nested attributes then addNestedAttributesToProject API should be used. *

*

* For more information, see Accessing Item Attributes in the Amazon DynamoDB Developer Guide. *

- * @param attributesToProject - * One or more attributes names to be retrieved from the database. + * + * @param attributesToProject One or more attributes names to be retrieved from the database. * @return Returns a reference to this object so that method calls can be chained together. */ public Builder attributesToProject(String... attributesToProject) { @@ -329,21 +353,85 @@ public Builder attributesToProject(String... attributesToProject) { *

* Adds a single attribute name to be retrieved from the database. This attribute can include * scalars, sets, or elements of a JSON document. + * If there are nested attributes then addNestedAttributesToProject API should be used. *

+ * + * @param attributeToProject An additional single attribute name to be retrieved from the database. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder addAttributeToProject(String attributeToProject) { + if (attributeToProject != null) { + addNestedAttributesToProject(NestedAttributeName.create(attributeToProject)); + } + return this; + } + + /** *

- * For more information, see Accessing Item Attributes in the Amazon DynamoDB Developer Guide. + * Adds a collection of the NestedAttributeNames to be retrieved from the database. These attributes can include + * scalars, sets, or elements of a JSON document. + * This method takes arguments in form of NestedAttributeName which supports representing nested attributes. + * The NestedAttributeNames is specially created for projecting Nested Attribute names. + * The DOT characters are not recognized as nesting separator by DDB thus for Enhanced request NestedAttributeNames + * should be created to project Nested Attribute name at various levels. + * This method will add new attributes to project to the existing list of attributes to project stored by this builder. + * + * @param nestedAttributeNames A collection of the attributes names to be retrieved from the database. + * Nested levels of Attributes can be added using NestedAttributeName class. + * Refer {@link NestedAttributeName}. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder addNestedAttributesToProject(Collection nestedAttributeNames) { + if (nestedAttributeNames != null) { + Validate.noNullElements(nestedAttributeNames, + "nestedAttributeNames list must not contain null elements"); + if (attributesToProject == null) { + this.attributesToProject = new ArrayList<>(nestedAttributeNames); + } else { + this.attributesToProject.addAll(nestedAttributeNames); + } + } + return this; + } + + /** + *

+ * Adds one or more attribute names to be retrieved from the database. These attributes can include + * scalars, sets, or elements of a JSON document. + * This method takes arguments in form of NestedAttributeName which supports representing nested attributes. + * This method takes arguments in form of NestedAttributeName which supports representing nested attributes. + * The NestedAttributeNames is specially created for projecting Nested Attribute names. + * The DOT characters are not recognized as nesting separator by DDB thus for Enhanced request NestedAttributeNames + * should be created to project Nested Attribute name at various levels. + * This method will add new attributes to project to the existing list of attributes to project stored + * by this builder. *

- * @param attributeToProject - * An additional single attribute name to be retrieved from the database. + * + * @param nestedAttributeNames One or more attributesNames to be retrieved from the database. + * Nested levels of Attributes can be added using NestedAttributeName class. + * Refer {@link NestedAttributeName}. * @return Returns a reference to this object so that method calls can be chained together. */ - public Builder addAttributeToProject(String attributeToProject) { - if (attributesToProject == null) { - attributesToProject = new ArrayList<>(); + public Builder addNestedAttributesToProject(NestedAttributeName... nestedAttributeNames) { + return addNestedAttributesToProject(Arrays.asList(nestedAttributeNames)); + } + + /** + *

+ * Adds a single NestedAttributeName to be retrieved from the database. This attribute can include + * scalars, sets, or elements of a JSON document. + * This method takes arguments in form of NestedAttributeName which supports representing nested attributes. + * This method will add new attributes to project to the existing list of attributes to project stored by this builder. + *

+ * + * @param nestedAttributeName An additional single attribute name to be retrieved from the database. + * Refer {@link NestedAttributeName}. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder addNestedAttributeToProject(NestedAttributeName nestedAttributeName) { + if (nestedAttributeName != null) { + addNestedAttributesToProject(Arrays.asList(nestedAttributeName)); } - attributesToProject.add(attributeToProject); return this; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequest.java index 1ec797d6512d..bc66597422a3 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequest.java @@ -22,10 +22,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.Validate; + /** * Defines parameters used to when scanning a DynamoDb table or index using the scan() operation (such as @@ -40,7 +44,7 @@ public final class ScanEnhancedRequest { private final Integer limit; private final Boolean consistentRead; private final Expression filterExpression; - private final List attributesToProject; + private final List attributesToProject; private ScanEnhancedRequest(Builder builder) { this.exclusiveStartKey = builder.exclusiveStartKey; @@ -64,10 +68,10 @@ public static Builder builder() { */ public Builder toBuilder() { return builder().exclusiveStartKey(exclusiveStartKey) - .limit(limit) - .consistentRead(consistentRead) - .filterExpression(filterExpression) - .attributesToProject(attributesToProject); + .limit(limit) + .consistentRead(consistentRead) + .filterExpression(filterExpression) + .addNestedAttributesToProject(attributesToProject); } /** @@ -99,12 +103,29 @@ public Expression filterExpression() { } /** - * Returns the list of projected attributes on this request object, or null if no projection is specified. + * Returns the list of projected attributes on this request object, or an null if no projection is specified. + * This is the single list which has Nested and Non Nested attributes to project. + * The Nested Attributes are represented using DOT separator in this List. + * Example : foo.bar is represented as "foo.bar" which is indistinguishable from a non-nested attribute + * with the name "foo.bar". + * Use {@link #nestedAttributesToProject} if you have a use-case that requires discrimination between these two cases. */ public List attributesToProject() { + return attributesToProject != null ? + attributesToProject.stream().map(item -> String.join(".", item.elements())) + .collect(Collectors.toList()) : null; + } + + /** + * Returns the list of projected attribute names, in the form of {@link NestedAttributeName} objects, + * for this request object, or null if no projection is specified. + * Refer {@link NestedAttributeName} + */ + public List nestedAttributesToProject() { return attributesToProject; } + @Override public boolean equals(Object o) { if (this == o) { @@ -127,9 +148,7 @@ public boolean equals(Object o) { return false; } if (attributesToProject != null - ? ! attributesToProject.equals(scan.attributesToProject) - : scan.attributesToProject != null - ) { + ? !attributesToProject.equals(scan.attributesToProject) : scan.attributesToProject != null) { return false; } return filterExpression != null ? filterExpression.equals(scan.filterExpression) : scan.filterExpression == null; @@ -153,7 +172,7 @@ public static final class Builder { private Integer limit; private Boolean consistentRead; private Expression filterExpression; - private List attributesToProject; + private List attributesToProject; private Builder() { } @@ -231,12 +250,18 @@ public Builder filterExpression(Expression filterExpression) { * "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html" * >Accessing Item Attributes in the Amazon DynamoDB Developer Guide. *

- * @param attributesToProject - * A collection of the attributes names to be retrieved from the database. + * + * @param attributesToProject A collection of the attributes names to be retrieved from the database. * @return Returns a reference to this object so that method calls can be chained together. */ public Builder attributesToProject(Collection attributesToProject) { - this.attributesToProject = attributesToProject != null ? new ArrayList<>(attributesToProject) : null; + if (this.attributesToProject != null) { + this.attributesToProject.clear(); + } + if (attributesToProject != null) { + addNestedAttributesToProject(new ArrayList<>(attributesToProject).stream() + .map(NestedAttributeName::create).collect(Collectors.toList())); + } return this; } @@ -254,8 +279,8 @@ public Builder attributesToProject(Collection attributesToProject) { * "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html" * >Accessing Item Attributes in the Amazon DynamoDB Developer Guide. *

- * @param attributesToProject - * One or more attributes names to be retrieved from the database. + * + * @param attributesToProject One or more attributes names to be retrieved from the database. * @return Returns a reference to this object so that method calls can be chained together. */ public Builder attributesToProject(String... attributesToProject) { @@ -272,15 +297,86 @@ public Builder attributesToProject(String... attributesToProject) { * "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html" * >Accessing Item Attributes in the Amazon DynamoDB Developer Guide. *

- * @param attributeToProject - * An additional single attribute name to be retrieved from the database. + * + * @param attributeToProject An additional single attribute name to be retrieved from the database. * @return Returns a reference to this object so that method calls can be chained together. */ public Builder addAttributeToProject(String attributeToProject) { - if (attributesToProject == null) { - attributesToProject = new ArrayList<>(); + if (attributeToProject != null) { + addNestedAttributesToProject(NestedAttributeName.create(attributeToProject)); + } + return this; + } + + /** + *

+ * Adds a collection of the NestedAttributeNames to be retrieved from the database. These attributes can include + * scalars, sets, or elements of a JSON document. + * This method takes arguments in form of NestedAttributeName which supports representing nested attributes. + * The NestedAttributeNames is specially created for projecting Nested Attribute names. + * The DOT characters are not recognized as nesting separator by DDB thus for Enhanced request NestedAttributeNames + * should be created to project Nested Attribute name at various levels. + * This method will add new attributes to project to the existing list of attributes to project stored by this builder. + * + * @param nestedAttributeNames A collection of the attributes names to be retrieved from the database. + * Nested levels of Attributes can be added using NestedAttributeName class. + * Refer {@link NestedAttributeName}. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder addNestedAttributesToProject(Collection nestedAttributeNames) { + if (nestedAttributeNames != null) { + Validate.noNullElements(nestedAttributeNames, + "nestedAttributeNames list must not contain null elements"); + if (attributesToProject == null) { + this.attributesToProject = new ArrayList<>(nestedAttributeNames); + } else { + this.attributesToProject.addAll(nestedAttributeNames); + } + } + return this; + } + + /** + *

+ * Add one or more attribute names to be retrieved from the database. These attributes can include + * scalars, sets, or elements of a JSON document. + * This method takes arguments in form of NestedAttributeName which supports representing nested attributes. + * This method takes arguments in form of NestedAttributeName which supports representing nested attributes. + * The NestedAttributeNames is specially created for projecting Nested Attribute names. + * The DOT characters are not recognized as nesting separator by DDB thus for Enhanced request NestedAttributeNames + * should be created to project Nested Attribute name at various levels. + * This method will add new attributes to project to the existing list of attributes to project stored by this builder. + * + * @param nestedAttributeNames One or more attributesNames to be retrieved from the database. + * Nested levels of Attributes can be added using NestedAttributeName class. + * Refer {@link NestedAttributeName}. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder addNestedAttributesToProject(NestedAttributeName... nestedAttributeNames) { + addNestedAttributesToProject(Arrays.asList(nestedAttributeNames)); + return this; + } + + /** + *

+ * Adds a single NestedAttributeName to be retrieved from the database. This attribute can include + * scalars, sets, or elements of a JSON document. + * This method takes arguments in form of NestedAttributeName which supports representing nested attributes. + *

+ *

+ * For more information, see Accessing Item Attributes in the Amazon DynamoDB Developer Guide. + *

+ * + * @param nestedAttributeName An additional single attribute name to be retrieved from the database. + * Refer {@link NestedAttributeName}. + * @return Returns a reference to this object so that method calls can be chained together. + */ + public Builder addNestedAttributeToProject(NestedAttributeName nestedAttributeName) { + if (nestedAttributeName != null) { + addNestedAttributesToProject(Arrays.asList(nestedAttributeName)); } - attributesToProject.add(attributeToProject); return this; } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/ProjectionExpressionConvertorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/ProjectionExpressionConvertorTest.java new file mode 100644 index 000000000000..4a967ae8c784 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/ProjectionExpressionConvertorTest.java @@ -0,0 +1,65 @@ +package software.amazon.awssdk.enhanced.dynamodb; + +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.internal.ProjectionExpressionConvertor; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.assertFails; + +public class ProjectionExpressionConvertorTest { + + public static final String MAPPED_INDICATOR = "#AMZN_MAPPED_"; + public static final String NESTING_SEPARATOR = "."; + + @Test + public void testAttributeNameWithNoNestedAttributes() { + final String keyName = "fieldKey"; + NestedAttributeName attributeName = NestedAttributeName.builder().elements(keyName).build(); + ProjectionExpressionConvertor expressionConvertor = ProjectionExpressionConvertor.create(Arrays.asList(attributeName)); + final Map stringStringMap = expressionConvertor.convertToExpressionMap(); + final Optional toNameExpression = expressionConvertor.convertToProjectionExpression(); + Map expectedmap = new HashMap<>(); + expectedmap.put(MAPPED_INDICATOR + keyName, keyName); + assertThat(stringStringMap).isEqualTo(expectedmap); + assertThat(toNameExpression.get()).contains((MAPPED_INDICATOR + keyName)); + } + + @Test + public void testAttributeNameWithNestedNestedAttributes() { + final String keyName = "fieldKey"; + final String nestedAttribute = "levelOne"; + NestedAttributeName attributeName = NestedAttributeName.builder().addElements(keyName, nestedAttribute).build(); + ProjectionExpressionConvertor expressionConvertor = ProjectionExpressionConvertor.create(Arrays.asList(attributeName)); + final Map stringStringMap = expressionConvertor.convertToExpressionMap(); + final Optional toNameExpression = expressionConvertor.convertToProjectionExpression(); + Map expectedmap = new HashMap<>(); + expectedmap.put(MAPPED_INDICATOR + keyName, keyName); + expectedmap.put(MAPPED_INDICATOR + nestedAttribute, nestedAttribute); + assertThat(stringStringMap).isEqualTo(expectedmap); + assertThat(toNameExpression.get()).contains(MAPPED_INDICATOR + keyName + NESTING_SEPARATOR + MAPPED_INDICATOR + nestedAttribute); + } + + @Test + public void testAttributeNameWithNullAttributeName() { + assertFails(() -> NestedAttributeName.builder().addElement(null).build()); + + } + + @Test + public void testAttributeNameWithNullElementsForNestingElement() { + assertFails(() -> NestedAttributeName.builder() + .elements("foo").addElement(null).build()); + } + + @Test + public void toBuilder() { + NestedAttributeName builtObject = NestedAttributeName.builder().addElement("foo").build(); + NestedAttributeName copiedObject = builtObject.toBuilder().build(); + assertThat(copiedObject).isEqualTo(builtObject); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicQueryTest.java index b60efcdc870f..b62bad8d9975 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicQueryTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicQueryTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasSize; @@ -27,23 +28,17 @@ import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortBetween; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.After; import org.junit.Before; import org.junit.Test; import software.amazon.awssdk.core.pagination.sync.SdkIterable; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; -import software.amazon.awssdk.enhanced.dynamodb.Expression; -import software.amazon.awssdk.enhanced.dynamodb.Key; -import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.*; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.PageIterable; import software.amazon.awssdk.enhanced.dynamodb.model.Page; @@ -52,6 +47,7 @@ import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; public class BasicQueryTest extends LocalDynamoDbSyncTestBase { + private static class Record { private String id; private Integer sort; @@ -121,19 +117,44 @@ public int hashCode() { .mapToObj(i -> new Record().setId("id-value").setSort(i).setValue(i)) .collect(Collectors.toList()); + private static final List NESTED_TEST_RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> { + final NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-"+i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v"+i); + return nestedTestRecord; + }) + .collect(Collectors.toList()); + private DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) .build(); private DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + private DynamoDbTable mappedNestedTable = enhancedClient.table(getConcreteTableName("nested-table-name"), + TableSchema.fromClass(NestedTestRecord.class)); + private void insertRecords() { RECORDS.forEach(record -> mappedTable.putItem(r -> r.item(record))); + NESTED_TEST_RECORDS.forEach(nestedTestRecord -> mappedNestedTable.putItem(r -> r.item(nestedTestRecord))); + } + + private void insertNestedRecords() { + NESTED_TEST_RECORDS.forEach(nestedTestRecord -> mappedNestedTable.putItem(r -> r.item(nestedTestRecord))); } @Before public void createTable() { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + mappedNestedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } @After @@ -141,6 +162,9 @@ public void deleteTable() { getDynamoDbClient().deleteTable(DeleteTableRequest.builder() .tableName(getConcreteTableName("table-name")) .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("nested-table-name")) + .build()); } @Test @@ -352,4 +376,192 @@ public void queryExclusiveStartKey_viaItems() { assertThat(results.stream().collect(Collectors.toList()), is(RECORDS.subList(8, 10))); } + + @Test + public void queryNestedRecord_SingleAttributeName() { + insertNestedRecords(); + Iterator> results = + mappedNestedTable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributeToProject(NestedAttributeName.builder().addElement("innerAttributeRecord") + .addElement("attribOne").build())).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(nullValue())); + assertThat(firstRecord.getSort(), is(nullValue())); + assertThat(firstRecord.getInnerAttributeRecord().getAttribOne(), is("attribOne-1")); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(nullValue())); + results = + mappedNestedTable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributeToProject(NestedAttributeName.create("sort")) + .addAttributeToProject("sort")).iterator(); + assertThat(results.hasNext(), is(true)); + page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(nullValue())); + assertThat(firstRecord.getSort(), is(1)); + assertThat(firstRecord.getInnerAttributeRecord(), is(nullValue())); + } + + + @Test + public void queryNestedRecord_withAttributeNameList() { + insertNestedRecords(); + Iterator> results = + mappedNestedTable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributesToProject(Arrays.asList( + NestedAttributeName.builder().elements("innerAttributeRecord", "attribOne").build(), + NestedAttributeName.builder().addElement("outerAttribOne").build())) + .addNestedAttributesToProject(NestedAttributeName.builder() + .addElements(Arrays.asList("innerAttributeRecord","attribTwo")).build())).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is("id-value-1")); + assertThat(firstRecord.getSort(), is(nullValue())); + assertThat(firstRecord.getInnerAttributeRecord().getAttribOne(), is("attribOne-1")); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(1)); + } + + + + + @Test + public void queryNestedRecord_withAttributeNameListAndStringAttributeToProjectAppended() { + insertNestedRecords(); + Iterator> results = + mappedNestedTable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributesToProject(Arrays.asList( + NestedAttributeName.builder().elements("innerAttributeRecord","attribOne").build())) + .addNestedAttributesToProject(NestedAttributeName.create("innerAttributeRecord","attribTwo")) + .addAttributeToProject("sort")).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(is(nullValue()))); + assertThat(firstRecord.getSort(), is(1)); + assertThat(firstRecord.getInnerAttributeRecord().getAttribOne(), is("attribOne-1")); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(1)); + } + + @Test + public void queryAllRecordsDefaultSettings_withNestedProjectionNamesNotInNameMap() { + insertNestedRecords(); + + Iterator> results = + mappedNestedTable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributeToProject( NestedAttributeName.builder().addElement("nonExistentSlot").build())).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord, is(nullValue())); + } + @Test + public void queryRecordDefaultSettings_withDotInTheName() { + insertNestedRecords(); + Iterator> results = + mappedNestedTable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .addNestedAttributeToProject( NestedAttributeName.create("test.com"))).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(is(nullValue()))); + assertThat(firstRecord.getSort(), is(is(nullValue()))); + assertThat(firstRecord.getInnerAttributeRecord() , is(nullValue())); + assertThat(firstRecord.getDotVariable(), is("v7")); + Iterator> resultWithAttributeToProject = + mappedNestedTable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject( "test.com").build()).iterator(); + assertThat(resultWithAttributeToProject.hasNext(), is(true)); + Page pageResult = resultWithAttributeToProject.next(); + assertThat(resultWithAttributeToProject.hasNext(), is(false)); + assertThat(pageResult.items().size(), is(1)); + NestedTestRecord record = pageResult.items().get(0); + assertThat(record.getOuterAttribOne(), is(is(nullValue()))); + assertThat(record.getSort(), is(is(nullValue()))); + assertThat(firstRecord.getInnerAttributeRecord() , is(nullValue())); + assertThat(record.getDotVariable(), is("v7")); + } + + @Test + public void queryRecordDefaultSettings_withEmptyAttributeList() { + insertNestedRecords(); + Iterator> results = + mappedNestedTable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject(new ArrayList<>()).build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is("id-value-7")); + assertThat(firstRecord.getSort(), is(7)); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(7)); + assertThat(firstRecord.getDotVariable(), is("v7")); + } + + @Test + public void queryRecordDefaultSettings_withNullAttributeList() { + insertNestedRecords(); + + List backwardCompatibilty = null; + + Iterator> results = + mappedNestedTable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject(backwardCompatibilty).build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is("id-value-7")); + assertThat(firstRecord.getSort(), is(7)); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(7)); + assertThat(firstRecord.getDotVariable(), is("v7")); + } + + @Test + public void queryAllRecordsDefaultSettings_withNestedProjectionNameEmptyNameMap() { + insertNestedRecords(); + + assertThatExceptionOfType(Exception.class).isThrownBy( + () -> { + Iterator> results = mappedNestedTable.query(b -> b.queryConditional( + keyEqualTo(k -> k.partitionValue("id-value-3"))) + .attributesToProject("").build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + }); + + assertThatExceptionOfType(Exception.class).isThrownBy( + () -> { + Iterator> results = mappedNestedTable.query(b -> b.queryConditional( + keyEqualTo(k -> k.partitionValue("id-value-3"))) + .addNestedAttributeToProject(NestedAttributeName.create("")).build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + + }); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicScanTest.java index caa6e855ed18..b11666510d3c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicScanTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/BasicScanTest.java @@ -15,32 +15,24 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.*; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; 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.primarySortKey; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.After; import org.junit.Before; import org.junit.Test; import software.amazon.awssdk.core.pagination.sync.SdkIterable; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; -import software.amazon.awssdk.enhanced.dynamodb.Expression; -import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.*; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.Page; import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; @@ -103,19 +95,45 @@ public int hashCode() { .mapToObj(i -> new Record().setId("id-value").setSort(i)) .collect(Collectors.toList()); + private static final List NESTED_TEST_RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> { + final NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-"+i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v"+i); + return nestedTestRecord; + }) + .collect(Collectors.toList()); + private DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) .build(); private DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + private DynamoDbTable mappedNestedTable = enhancedClient.table(getConcreteTableName("nested-table-name"), + TableSchema.fromClass(NestedTestRecord.class)); + + private void insertRecords() { RECORDS.forEach(record -> mappedTable.putItem(r -> r.item(record))); } + private void insertNestedRecords() { + NESTED_TEST_RECORDS.forEach(nestedTestRecord -> mappedNestedTable.putItem(r -> r.item(nestedTestRecord))); + } + + @Before public void createTable() { mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + mappedNestedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } @After @@ -123,6 +141,9 @@ public void deleteTable() { getDynamoDbClient().deleteTable(DeleteTableRequest.builder() .tableName(getConcreteTableName("table-name")) .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("nested-table-name")) + .build()); } @Test @@ -292,4 +313,322 @@ private Map getKeyMap(int sort) { result.put("sort", numberValue(sort)); return Collections.unmodifiableMap(result); } + @Test + public void scanAllRecordsWithFilterAndNestedProjectionSingleAttribute() { + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator> results = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject( + NestedAttributeName.create(Arrays.asList("innerAttributeRecord","attribOne"))) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getInnerAttributeRecord().getAttribOne() + .compareTo(item2.getInnerAttributeRecord().getAttribOne())); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(nullValue())); + assertThat(firstRecord.getSort(), is(nullValue())); + assertThat(firstRecord.getInnerAttributeRecord().getAttribOne(), is("attribOne-3")); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(nullValue())); + + //Attribute repeated with new and old attributeToProject + results = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.create("sort")) + .addAttributeToProject("sort") + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getSort() + .compareTo(item2.getSort())); + firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(nullValue())); + assertThat(firstRecord.getSort(), is(3)); + assertThat(firstRecord.getInnerAttributeRecord(), is(nullValue())); + assertThat(firstRecord.getInnerAttributeRecord(), is(nullValue())); + + results = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributeToProject( + NestedAttributeName.create(Arrays.asList("innerAttributeRecord","attribOne"))) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getInnerAttributeRecord().getAttribOne() + .compareTo(item2.getInnerAttributeRecord().getAttribOne())); + firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(nullValue())); + assertThat(firstRecord.getSort(), is(nullValue())); + assertThat(firstRecord.getInnerAttributeRecord().getAttribOne(), is("attribOne-3")); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(nullValue())); + } + + @Test + public void scanAllRecordsWithFilterAndNestedProjectionMultipleAttribute() { + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + final ScanEnhancedRequest build = ScanEnhancedRequest.builder() + .filterExpression(expression) + .addAttributeToProject("outerAttribOne") + .addNestedAttributesToProject(Arrays.asList(NestedAttributeName.builder().elements("innerAttributeRecord") + .addElement("attribOne").build())) + .addNestedAttributeToProject(NestedAttributeName.builder() + .elements(Arrays.asList("innerAttributeRecord", "attribTwo")).build()) + .build(); + Iterator> results = + mappedNestedTable.scan( + build + ).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getInnerAttributeRecord().getAttribOne() + .compareTo(item2.getInnerAttributeRecord().getAttribOne())); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is("id-value-3")); + assertThat(firstRecord.getSort(), is(nullValue())); + assertThat(firstRecord.getInnerAttributeRecord().getAttribOne(), is("attribOne-3")); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(3)); + + } + + @Test + public void scanAllRecordsWithNonExistigKeyName() { + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + + Iterator> results = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder().addElement("nonExistent").build()) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord, is(nullValue())); + } + + @Test + public void scanAllRecordsWithDotInAttributeKeyName() { + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator> results = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName + .create("test.com")).build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getDotVariable() + .compareTo(item2.getDotVariable())); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(nullValue())); + assertThat(firstRecord.getSort(), is(nullValue())); + assertThat(firstRecord.getDotVariable(), is("v3")); + assertThat(firstRecord.getInnerAttributeRecord(), is(nullValue())); + assertThat(firstRecord.getInnerAttributeRecord(), is(nullValue())); + } + + @Test + public void scanAllRecordsWithSameNamesRepeated() { + //Attribute repeated with new and old attributeToProject + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator >results = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder().elements("sort").build()) + .addAttributeToProject("sort") + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getSort() + .compareTo(item2.getSort())); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is(nullValue())); + assertThat(firstRecord.getSort(), is(3)); + assertThat(firstRecord.getInnerAttributeRecord(), is(nullValue())); + assertThat(firstRecord.getInnerAttributeRecord(), is(nullValue())); + } + + @Test + public void scanAllRecordsWithEmptyList() { + //Attribute repeated with new and old attributeToProject + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator >results = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(new ArrayList<>()) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getSort() + .compareTo(item2.getSort())); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is("id-value-3")); + assertThat(firstRecord.getSort(), is(3)); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(3)); + assertThat(firstRecord.getInnerAttributeRecord().getAttribOne(), is("attribOne-3")); + } + + @Test + public void scanAllRecordsWithNullAttributesToProject() { + //Attribute repeated with new and old attributeToProject + insertNestedRecords(); + List backwardCompatibilityNull = null; + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator >results = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .attributesToProject("test.com") + .attributesToProject(backwardCompatibilityNull) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getSort() + .compareTo(item2.getSort())); + NestedTestRecord firstRecord = page.items().get(0); + assertThat(firstRecord.getOuterAttribOne(), is("id-value-3")); + assertThat(firstRecord.getSort(), is(3)); + assertThat(firstRecord.getInnerAttributeRecord().getAttribTwo(), is(3)); + assertThat(firstRecord.getInnerAttributeRecord().getAttribOne(), is("attribOne-3")); + } + + @Test + public void scanAllRecordsWithNestedProjectionNameEmptyNameMap() { + insertNestedRecords(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + final Iterator> results = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder().elements("").build()).build() + ).iterator(); + + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { final boolean b = results.hasNext(); + Page next = results.next(); }).withMessageContaining("ExpressionAttributeNames contains invalid value"); + + final Iterator> resultsAttributeToProject = + mappedNestedTable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addAttributeToProject("").build() + ).iterator(); + + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { + final boolean b = resultsAttributeToProject.hasNext(); + Page next = resultsAttributeToProject.next(); + }); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverter.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverter.java new file mode 100755 index 000000000000..a006e0f4bf0f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverter.java @@ -0,0 +1,81 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.HashMap; +import java.util.Map; + +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +/** + * Event Payload Converter to save the record on the class + */ +public class InnerAttribConverter implements AttributeConverter { + + private final ObjectMapper objectMapper; + + /** + * This No Args constuctor is needed by the DynamoDbConvertedBy annotation + */ + public InnerAttribConverter() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + final AttributeValue dd = stringValue("dd"); + AttributeConverter attributeConverter = null; + AttributeValueType attributeValueType = null; + EnhancedType enhancedType = null; + // add this to preserve the same offset (don't convert to UTC) + this.objectMapper.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false); + } + + @Override + public AttributeValue transformFrom(final T input) { + + Map map = null; + if (input != null) { + map = new HashMap<>(); + InnerAttributeRecord innerAttributeRecord = (InnerAttributeRecord) input; + if (innerAttributeRecord.getAttribOne() != null) { + + final AttributeValue attributeValue = stringValue(innerAttributeRecord.getAttribOne()); + map.put("attribOne", stringValue(innerAttributeRecord.getAttribOne())); + } + if (innerAttributeRecord.getAttribTwo() != null) { + map.put("attribTwo", stringValue(String.valueOf(innerAttributeRecord.getAttribTwo()))); + } + } + return AttributeValue.builder().m(map).build(); + + } + + @Override + public T transformTo(final AttributeValue attributeValue) { + InnerAttributeRecord innerMetadata = new InnerAttributeRecord(); + if (attributeValue.m().get("attribOne") != null) { + innerMetadata.setAttribOne(attributeValue.m().get("attribOne").s()); + } + if (attributeValue.m().get("attribTwo") != null) { + innerMetadata.setAttribTwo(Integer.valueOf(attributeValue.m().get("attribTwo").s())); + } + return (T) innerMetadata; + } + + @Override + public EnhancedType type() { + return (EnhancedType) EnhancedType.of(InnerAttributeRecord.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.S; + } + + +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java new file mode 100755 index 000000000000..b38cc7554a37 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java @@ -0,0 +1,18 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + + +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; + +/** + * InnerAttribConverterProvider to save the InnerAttribConverter on the class. + */ +public class InnerAttribConverterProvider implements AttributeConverterProvider { + + + @Override + public AttributeConverter converterFor(EnhancedType enhancedType) { + return new InnerAttribConverter(); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttributeRecord.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttributeRecord.java new file mode 100755 index 000000000000..6edd5112ff94 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttributeRecord.java @@ -0,0 +1,34 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + + +public class InnerAttributeRecord { + private String attribOne; + private Integer attribTwo; + + @DynamoDbPartitionKey + public String getAttribOne() { + return attribOne; + } + + public void setAttribOne(String attribOne) { + this.attribOne = attribOne; + } + + public Integer getAttribTwo() { + return attribTwo; + } + + public void setAttribTwo(Integer attribTwo) { + this.attribTwo = attribTwo; + } + + @Override + public String toString() { + return "InnerAttributeRecord{" + + "attribOne='" + attribOne + '\'' + + ", attribTwo=" + attribTwo + + '}'; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedTestRecord.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedTestRecord.java new file mode 100755 index 000000000000..b1122efbc0f0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedTestRecord.java @@ -0,0 +1,62 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.*; + + +@DynamoDbBean +public class NestedTestRecord { + private String outerAttribOne; + private Integer sort; + private InnerAttributeRecord innerAttributeRecord; + + private String dotVariable; + + + @DynamoDbPartitionKey + public String getOuterAttribOne() { + return outerAttribOne; + } + + public void setOuterAttribOne(String outerAttribOne) { + this.outerAttribOne = outerAttribOne; + } + + @DynamoDbSortKey + public Integer getSort() { + return sort; + } + + public void setSort(Integer sort) { + this.sort = sort; + } + + + + @DynamoDbConvertedBy(InnerAttribConverter.class) + public InnerAttributeRecord getInnerAttributeRecord() { + return innerAttributeRecord; + } + + public void setInnerAttributeRecord(InnerAttributeRecord innerAttributeRecord) { + this.innerAttributeRecord = innerAttributeRecord; + } + + @DynamoDbAttribute("test.com") + public String getDotVariable() { + return dotVariable; + } + + public void setDotVariable(String dotVariable) { + this.dotVariable = dotVariable; + } + + @Override + public String toString() { + return "NestedTestRecord{" + + "outerAttribOne='" + outerAttribOne + '\'' + + ", sort=" + sort + + ", innerAttributeRecord=" + innerAttributeRecord + + ", dotVariable='" + dotVariable + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryEnhancedRequestTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryEnhancedRequestTest.java index 2b146cf3f194..827113e1f72b 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryEnhancedRequestTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryEnhancedRequestTest.java @@ -15,25 +15,24 @@ package software.amazon.awssdk.enhanced.dynamodb.model; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.*; + import static java.util.Collections.singletonMap; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.assertFails; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -import software.amazon.awssdk.enhanced.dynamodb.Expression; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; - @RunWith(MockitoJUnitRunner.class) public class QueryEnhancedRequestTest { @@ -58,9 +57,9 @@ public void builder_maximal() { Map expressionValues = singletonMap(":test-key", stringValue("test-value")); Expression filterExpression = Expression.builder() - .expression("test-expression") - .expressionValues(expressionValues) - .build(); + .expression("test-expression") + .expressionValues(expressionValues) + .build(); QueryConditional queryConditional = keyEqualTo(k -> k.partitionValue("id-value")); @@ -70,15 +69,15 @@ public void builder_maximal() { attributesToProject.add(additionalElement); QueryEnhancedRequest builtObject = QueryEnhancedRequest.builder() - .exclusiveStartKey(exclusiveStartKey) - .consistentRead(false) - .filterExpression(filterExpression) - .limit(3) - .queryConditional(queryConditional) - .scanIndexForward(true) - .attributesToProject(attributesToProjectArray) - .addAttributeToProject(additionalElement) - .build(); + .exclusiveStartKey(exclusiveStartKey) + .consistentRead(false) + .filterExpression(filterExpression) + .limit(3) + .queryConditional(queryConditional) + .scanIndexForward(true) + .attributesToProject(attributesToProjectArray) + .addAttributeToProject(additionalElement) + .build(); assertThat(builtObject.exclusiveStartKey(), is(exclusiveStartKey)); assertThat(builtObject.consistentRead(), is(false)); @@ -89,6 +88,95 @@ public void builder_maximal() { assertThat(builtObject.attributesToProject(), is(attributesToProject)); } + + @Test + public void test_withNestedAttributeAddedFirstAndThenAttributesToProject() { + + String[] attributesToProjectArray = {"one", "two"}; + String additionalElement = "three"; + QueryEnhancedRequest builtObject = QueryEnhancedRequest.builder() + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar")) + .attributesToProject(attributesToProjectArray) + .addAttributeToProject(additionalElement) + .build(); + List attributesToProject = Arrays.asList("one", "two", "three"); + assertThat(builtObject.attributesToProject(), is(attributesToProject)); + } + + + @Test + public void test_nestedAttributesToProjectWithNestedAttributeAddedLast() { + + String[] attributesToProjectArray = {"one", "two"}; + String additionalElement = "three"; + + QueryEnhancedRequest builtObjectOne = QueryEnhancedRequest.builder() + .attributesToProject(attributesToProjectArray) + .addAttributeToProject(additionalElement) + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar")) + .build(); + List attributesToProjectNestedLast = Arrays.asList("one", "two", "three", "foo.bar"); + assertThat(builtObjectOne.attributesToProject(), is(attributesToProjectNestedLast)); + + } + + @Test + public void test_nestedAttributesToProjectWithNestedAttributeAddedInBetween() { + + String[] attributesToProjectArray = {"one", "two"}; + String additionalElement = "three"; + + QueryEnhancedRequest builtObjectOne = QueryEnhancedRequest.builder() + .attributesToProject(attributesToProjectArray) + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar")) + .addAttributeToProject(additionalElement) + .build(); + List attributesToProjectNestedLast = Arrays.asList("one", "two", "foo.bar", "three"); + assertThat(builtObjectOne.attributesToProject(), is(attributesToProjectNestedLast)); + + } + + @Test + public void test_nestedAttributesToProjectOverwrite() { + + String[] attributesToProjectArray = {"one", "two"}; + String additionalElement = "three"; + String[] overwrite = { "overwrite"}; + + QueryEnhancedRequest builtObjectTwo = QueryEnhancedRequest.builder() + .attributesToProject(attributesToProjectArray) + .addAttributeToProject(additionalElement) + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar")) + .attributesToProject(overwrite) + .build(); + assertThat(builtObjectTwo.attributesToProject(), is(Arrays.asList(overwrite))); + } + + @Test + public void test_nestedAttributesNullNestedAttributeElement() { + List attributeNames = new ArrayList<>(); + attributeNames.add(NestedAttributeName.create("foo")); + attributeNames.add(null); + assertFails(() -> QueryEnhancedRequest.builder() + .addNestedAttributesToProject(attributeNames) + .build()); + + assertFails(() -> QueryEnhancedRequest.builder() + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar"), null) + .build()); + + NestedAttributeName nestedAttributeName = null; + QueryEnhancedRequest.builder() + .addNestedAttributeToProject(nestedAttributeName) + .build(); + assertFails(() -> QueryEnhancedRequest.builder() + .addNestedAttributesToProject(nestedAttributeName) + .build()); + } + + + + @Test public void toBuilder() { QueryEnhancedRequest builtObject = QueryEnhancedRequest.builder().build(); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequestTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequestTest.java index ce2a86f443c6..04d77c7d6090 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequestTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/model/ScanEnhancedRequestTest.java @@ -19,6 +19,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.assertFails; +import static software.amazon.awssdk.enhanced.dynamodb.converters.attribute.ConverterTestUtils.transformTo; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; @@ -31,6 +33,8 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnhancedAttributeValue; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @RunWith(MockitoJUnitRunner.class) @@ -80,6 +84,108 @@ public void builder_maximal() { assertThat(builtObject.limit(), is(3)); } + @Test + public void test_withNestedAttributeAddedFirst() { + + String[] attributesToProjectArray = {"one", "two"}; + String additionalElement = "three"; + ScanEnhancedRequest builtObject = ScanEnhancedRequest.builder() + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar")) + .attributesToProject(attributesToProjectArray) + .addAttributeToProject(additionalElement) + .build(); + List attributesToProject = Arrays.asList("one", "two", "three"); + assertThat(builtObject.attributesToProject(), is(attributesToProject)); + } + + + @Test + public void test_nestedAttributesToProjectWithNestedAttributeAddedLast() { + + String[] attributesToProjectArray = {"one", "two"}; + String additionalElement = "three"; + + ScanEnhancedRequest builtObjectOne = ScanEnhancedRequest.builder() + .attributesToProject(attributesToProjectArray) + .addAttributeToProject(additionalElement) + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar")) + .build(); + List attributesToProjectNestedLast = Arrays.asList("one", "two", "three", "foo.bar"); + assertThat(builtObjectOne.attributesToProject(), is(attributesToProjectNestedLast)); + + } + + @Test + public void test_nestedAttributesToProjectWithNestedAttributeAddedInBetween() { + + String[] attributesToProjectArray = {"one", "two"}; + String additionalElement = "three"; + + ScanEnhancedRequest builtObjectOne = ScanEnhancedRequest.builder() + .attributesToProject(attributesToProjectArray) + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar")) + .addAttributeToProject(additionalElement) + .build(); + List attributesToProjectNestedLast = Arrays.asList("one", "two", "foo.bar", "three"); + assertThat(builtObjectOne.attributesToProject(), is(attributesToProjectNestedLast)); + + } + @Test + public void test_nestedAttributesToProjectOverwrite() { + + String[] attributesToProjectArray = {"one", "two"}; + String additionalElement = "three"; + String[] overwrite = { "overwrite"}; + + ScanEnhancedRequest builtObjectTwo = ScanEnhancedRequest.builder() + .attributesToProject(attributesToProjectArray) + .addAttributeToProject(additionalElement) + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar")) + .attributesToProject(overwrite) + .build(); + assertThat(builtObjectTwo.attributesToProject(), is(Arrays.asList(overwrite))); + } + + @Test + public void test_nestedAttributesNullStringElement() { + + String[] attributesToProjectArray = {"one", "two", null}; + String additionalElement = "three"; + assertFails(() -> ScanEnhancedRequest.builder() + .attributesToProject(attributesToProjectArray) + .addAttributeToProject(additionalElement) + .addAttributeToProject(null) + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar")) + .build()); + + assertFails(() -> ScanEnhancedRequest.builder() + .attributesToProject("foo", "bar", null) + .build()); + + } + + @Test + public void test_nestedAttributesNullNestedAttributeElement() { + List attributeNames = new ArrayList<>(); + attributeNames.add(NestedAttributeName.create("foo")); + attributeNames.add(null); + assertFails(() -> ScanEnhancedRequest.builder() + .addNestedAttributesToProject(attributeNames) + .build()); + assertFails(() -> ScanEnhancedRequest.builder() + .addNestedAttributesToProject(NestedAttributeName.create("foo", "bar"), null) + .build()); + NestedAttributeName nestedAttributeName = null; + ScanEnhancedRequest.builder() + .addNestedAttributeToProject(nestedAttributeName) + .build(); + assertFails(() -> ScanEnhancedRequest.builder() + .addNestedAttributesToProject(nestedAttributeName) + .build()); + } + + + @Test public void toBuilder() { ScanEnhancedRequest builtObject = ScanEnhancedRequest.builder().exclusiveStartKey(null).build();