From 629146af616d0aa6c9d3909f41c4f2fbc1ba3a16 Mon Sep 17 00:00:00 2001 From: Anna-Karin Salander Date: Tue, 28 Feb 2023 15:48:55 -0800 Subject: [PATCH] temp --- .../operations/UpdateItemOperation.java | 42 +-- .../update/UpdateExpressionConverter.java | 7 +- .../update/UpdateExpressionResolver.java | 142 ++++++++ .../update/UpdateExpressionUtils.java | 26 +- .../TransactUpdateItemEnhancedRequest.java | 49 +++ .../model/UpdateItemEnhancedRequest.java | 47 +++ .../dynamodb/update/UpdateExpression.java | 4 + .../UpdateExpressionRequestTest.java | 330 ++++++++++++++++++ .../functionaltests/UpdateExpressionTest.java | 176 +++++++--- .../models/RecordForUpdateExpressions.java | 10 + .../update/UpdateExpressionConverterTest.java | 4 +- 11 files changed, 745 insertions(+), 92 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionResolver.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateExpressionRequestTest.java diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java index 3c032740dd9c..f5baa5609541 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java @@ -16,11 +16,9 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.readAndTransformSingleItem; -import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.operationExpression; import static software.amazon.awssdk.utils.CollectionUtils.filterMap; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -34,6 +32,7 @@ import software.amazon.awssdk.enhanced.dynamodb.extensions.WriteModification; import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext; import software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionResolver; import software.amazon.awssdk.enhanced.dynamodb.model.TransactUpdateItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse; @@ -113,7 +112,7 @@ public UpdateItemRequest generateRequest(TableSchema tableSchema, Map keyAttributes = filterMap(itemMap, entry -> primaryKeys.contains(entry.getKey())); Map nonKeyAttributes = filterMap(itemMap, entry -> !primaryKeys.contains(entry.getKey())); - Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, nonKeyAttributes); + Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, request, nonKeyAttributes); Expression conditionExpression = generateConditionExpressionIfExist(transformation, request); Map expressionNames = coalesceExpressionNames(updateExpression, conditionExpression); @@ -205,23 +204,26 @@ public TransactWriteItem generateTransactWriteItem(TableSchema tableSchema, O * if there are attributes to be updated (most likely). If both exist, they are merged and the code generates a final * Expression that represent the result. */ - private Expression generateUpdateExpressionIfExist(TableMetadata tableMetadata, - WriteModification transformation, - Map attributes) { - UpdateExpression updateExpression = null; - if (transformation != null && transformation.updateExpression() != null) { - updateExpression = transformation.updateExpression(); - } - if (!attributes.isEmpty()) { - List nonRemoveAttributes = UpdateExpressionConverter.findAttributeNames(updateExpression); - UpdateExpression operationUpdateExpression = operationExpression(attributes, tableMetadata, nonRemoveAttributes); - if (updateExpression == null) { - updateExpression = operationUpdateExpression; - } else { - updateExpression = UpdateExpression.mergeExpressions(updateExpression, operationUpdateExpression); - } - } - return UpdateExpressionConverter.toExpression(updateExpression); + private Expression generateUpdateExpressionIfExist( + TableMetadata tableMetadata, + WriteModification transformation, + Either, TransactUpdateItemEnhancedRequest> request, + Map nonKeyAttributes) { + + UpdateExpression requestUpdateExpression = request.map(r -> Optional.ofNullable(r.updateExpression()), + r -> Optional.ofNullable(r.updateExpression())) + .orElse(null); + + UpdateExpressionResolver updateExpressionResolver = + UpdateExpressionResolver.builder() + .tableMetadata(tableMetadata) + .itemNonKeyAttributes(nonKeyAttributes) + .requestExpression(requestUpdateExpression) + .transformationExpression(transformation != null ? transformation.updateExpression() : null) + .build(); + + UpdateExpression mergedUpdateExpression = updateExpressionResolver.resolve(); + return UpdateExpressionConverter.toExpression(mergedUpdateExpression); } /** diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverter.java index d3c3c748e563..587912c26fbb 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverter.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverter.java @@ -76,7 +76,7 @@ private UpdateExpressionConverter() { * @return an Expression representing the concatenation of all actions in this UpdateExpression */ public static Expression toExpression(UpdateExpression expression) { - if (expression == null) { + if (expression == null || expression.isEmpty()) { return null; } Map expressionValues = mergeExpressionValues(expression); @@ -91,8 +91,9 @@ public static Expression toExpression(UpdateExpression expression) { } /** - * Attempts to find the list of attribute names that will be updated for the supplied {@link UpdateExpression} by looking at - * the combined collection of paths and ExpressionName values. Because attribute names can be composed from nested + * Attempts to find the list of attributes associated with update actions for the supplied {@link UpdateExpression} by + * looking at + * the combined collection of paths and ExpressionName values. Because attribute names can be composed of nested * attribute references and list references, the leftmost part will be returned if composition is detected. *

* Examples: The expression contains a {@link DeleteAction} with a path value of 'MyAttribute[1]'; the list returned diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionResolver.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionResolver.java new file mode 100644 index 000000000000..6eae1f649cae --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionResolver.java @@ -0,0 +1,142 @@ +/* + * 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.update; + +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.removeActionsFor; +import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.setActionsFor; +import static software.amazon.awssdk.utils.CollectionUtils.filterMap; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * + */ +@SdkInternalApi +public final class UpdateExpressionResolver { + + private final UpdateExpression extensionExpression; + private final UpdateExpression requestExpression; + private final Map itemNonKeyAttributes; + private final TableMetadata tableMetadata; + + private UpdateExpressionResolver(Builder builder) { + this.extensionExpression = builder.transformationExpression; + this.requestExpression = builder.requestExpression; + this.itemNonKeyAttributes = builder.nonKeyAttributes; + this.tableMetadata = builder.tableMetadata; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Resolves all available and potential update expressions by priority and returns a merged update expression. It may + * return null, if the item attribute map is empty / does not contain non-null attributes and no other update expressions + * are present. + *

+ * Conditions that will result in error: + *

    + *
  • Two expressions contain actions referencing the same attribute
  • + *
+ *

+ * Note: The presence of attributes in update expressions submitted through the request or generated from extensions + * take precedence over removing attributes based on item configuration. + * For example, when IGNORE_NULLS is set to true (default), the client generates REMOVE actions for all + * attributes in the schema that are not explicitly set in the request item submitted to the operation. If such + * attributes are referenced in update expressions on the request or from extensions, the remove actions are filtered + * out. + */ + public UpdateExpression resolve() { + UpdateExpression itemSetExpression = generateItemSetExpression(itemNonKeyAttributes, tableMetadata); + + List nonRemoveAttributes = attributesPresentInExpressions(Arrays.asList(extensionExpression, requestExpression)); + UpdateExpression itemRemoveExpression = generateItemRemoveExpression(itemNonKeyAttributes, nonRemoveAttributes); + + UpdateExpression itemExpression = UpdateExpression.mergeExpressions(itemSetExpression, itemRemoveExpression); + UpdateExpression extensionItemExpression = UpdateExpression.mergeExpressions(extensionExpression, itemExpression); + return UpdateExpression.mergeExpressions(requestExpression, extensionItemExpression); + } + + private static List attributesPresentInExpressions(List updateExpressions) { + return updateExpressions.stream() + .map(UpdateExpressionConverter::findAttributeNames) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + public static UpdateExpression generateItemSetExpression(Map itemMap, + TableMetadata tableMetadata) { + + Map setAttributes = filterMap(itemMap, e -> !isNullAttributeValue(e.getValue())); + return UpdateExpression.builder() + .actions(setActionsFor(setAttributes, tableMetadata)) + .build(); + } + + public static UpdateExpression generateItemRemoveExpression(Map itemMap, + List nonRemoveAttributes) { + Map removeAttributes = + filterMap(itemMap, e -> isNullAttributeValue(e.getValue()) && !nonRemoveAttributes.contains(e.getKey())); + + return UpdateExpression.builder() + .actions(removeActionsFor(removeAttributes)) + .build(); + } + + public static final class Builder { + + private TableMetadata tableMetadata; + private UpdateExpression transformationExpression; + private UpdateExpression requestExpression; + private Map nonKeyAttributes; + + public Builder tableMetadata(TableMetadata tableMetadata) { + this.tableMetadata = tableMetadata; + return this; + } + + public Builder transformationExpression(UpdateExpression transformationExpression) { + this.transformationExpression = transformationExpression; + return this; + } + + public Builder itemNonKeyAttributes(Map nonKeyAttributes) { + this.nonKeyAttributes = nonKeyAttributes; + return this; + } + + public Builder requestExpression(UpdateExpression requestExpression) { + this.requestExpression = requestExpression; + return this; + } + + public UpdateExpressionResolver build() { + return new UpdateExpressionResolver(this); + } + + } + + +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionUtils.java index 41991e0e2865..c44ab6213d1a 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionUtils.java @@ -49,32 +49,10 @@ public static String ifNotExists(String key, String initValue) { return "if_not_exists(" + keyRef(key) + ", " + valueRef(initValue) + ")"; } - /** - * Generates an UpdateExpression representing a POJO, with only SET and REMOVE actions. - */ - public static UpdateExpression operationExpression(Map itemMap, - TableMetadata tableMetadata, - List nonRemoveAttributes) { - - Map setAttributes = filterMap(itemMap, e -> !isNullAttributeValue(e.getValue())); - UpdateExpression setAttributeExpression = UpdateExpression.builder() - .actions(setActionsFor(setAttributes, tableMetadata)) - .build(); - - Map removeAttributes = - filterMap(itemMap, e -> isNullAttributeValue(e.getValue()) && !nonRemoveAttributes.contains(e.getKey())); - - UpdateExpression removeAttributeExpression = UpdateExpression.builder() - .actions(removeActionsFor(removeAttributes)) - .build(); - - return UpdateExpression.mergeExpressions(setAttributeExpression, removeAttributeExpression); - } - /** * Creates a list of SET actions for all attributes supplied in the map. */ - private static List setActionsFor(Map attributesToSet, TableMetadata tableMetadata) { + public static List setActionsFor(Map attributesToSet, TableMetadata tableMetadata) { return attributesToSet.entrySet() .stream() .map(entry -> setValue(entry.getKey(), @@ -86,7 +64,7 @@ private static List setActionsFor(Map attribu /** * Creates a list of REMOVE actions for all attributes supplied in the map. */ - private static List removeActionsFor(Map attributesToSet) { + public static List removeActionsFor(Map attributesToSet) { return attributesToSet.entrySet() .stream() .map(entry -> remove(entry.getKey())) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactUpdateItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactUpdateItemEnhancedRequest.java index 7fec8372ba2e..a4c8553836c6 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactUpdateItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactUpdateItemEnhancedRequest.java @@ -22,6 +22,11 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.update.AddAction; +import software.amazon.awssdk.enhanced.dynamodb.update.DeleteAction; +import software.amazon.awssdk.enhanced.dynamodb.update.RemoveAction; +import software.amazon.awssdk.enhanced.dynamodb.update.SetAction; +import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression; import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; /** @@ -41,12 +46,14 @@ public class TransactUpdateItemEnhancedRequest { private final T item; private final Boolean ignoreNulls; private final Expression conditionExpression; + private final UpdateExpression updateExpression; private final String returnValuesOnConditionCheckFailure; private TransactUpdateItemEnhancedRequest(Builder builder) { this.item = builder.item; this.ignoreNulls = builder.ignoreNulls; this.conditionExpression = builder.conditionExpression; + this.updateExpression = builder.updateExpression; this.returnValuesOnConditionCheckFailure = builder.returnValuesOnConditionCheckFailure; } @@ -92,6 +99,13 @@ public Expression conditionExpression() { return conditionExpression; } + /** + * Returns the update expression {@link UpdateExpression} set on this request object, or null if it doesn't exist. + */ + public UpdateExpression updateExpression() { + return updateExpression; + } + /** * Returns what values to return if the condition check fails. *

@@ -140,6 +154,9 @@ public boolean equals(Object o) { if (!Objects.equals(conditionExpression, that.conditionExpression)) { return false; } + if (!Objects.equals(updateExpression, that.updateExpression)) { + return false; + } return Objects.equals(returnValuesOnConditionCheckFailure, that.returnValuesOnConditionCheckFailure); } @@ -148,6 +165,7 @@ public int hashCode() { int result = Objects.hashCode(item); result = 31 * result + Objects.hashCode(ignoreNulls); result = 31 * result + Objects.hashCode(conditionExpression); + result = 31 * result + Objects.hashCode(updateExpression); result = 31 * result + Objects.hashCode(returnValuesOnConditionCheckFailure); return result; } @@ -162,6 +180,8 @@ public static final class Builder { private T item; private Boolean ignoreNulls; private Expression conditionExpression; + + private UpdateExpression updateExpression; private String returnValuesOnConditionCheckFailure; private Builder() { @@ -208,6 +228,35 @@ public Builder item(T item) { return this; } + /** + * Define an {@link UpdateExpression} to control updating specific parts of the item in DynamoDb. The update expression + * corresponds to the DynamoDb update expression format. It can be used to set, modify and delete attributes for + * use cases that simply supplying the item does not cover; in particular, manipulating composed attributes such as + * sets or lists: + *

    + *
  • Add/remove elements to/from list attributes
  • + *
  • Add/remove elements to/from set attributes
  • + *
  • Unset or nullify attributes without modifying the whole attribute
  • + *
+ *

+ * This method will throw an exception if the expression references an attribute that is already present on the + * item, or is modified through an extension. + *

+ * Note: This is a powerful mechanism that bypasses many of the abstractions and + * safety checks in the enhanced client, and should be used with caution. Only use it when submitting only + * a configured item bean/object is insufficient. + *

+ * See {@link UpdateExpression}, {@link AddAction}, {@link DeleteAction}, {@link SetAction} and + * {@link RemoveAction} for syntax and examples. + * + * @param updateExpression a composed expression of type {@link UpdateExpression} + * @return a builder of this type + */ + public Builder updateExpression(UpdateExpression updateExpression) { + this.updateExpression = updateExpression; + return this; + } + /** * Use ReturnValuesOnConditionCheckFailure to get the item attributes if the ConditionCheck * condition fails. For ReturnValuesOnConditionCheckFailure, the valid values are: NONE and diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java index 796c9db95abf..838e5afd1759 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java @@ -22,6 +22,11 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.update.AddAction; +import software.amazon.awssdk.enhanced.dynamodb.update.DeleteAction; +import software.amazon.awssdk.enhanced.dynamodb.update.RemoveAction; +import software.amazon.awssdk.enhanced.dynamodb.update.SetAction; +import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression; import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; @@ -44,6 +49,7 @@ public final class UpdateItemEnhancedRequest { private final T item; private final Boolean ignoreNulls; private final Expression conditionExpression; + private final UpdateExpression updateExpression; private final String returnConsumedCapacity; private final String returnItemCollectionMetrics; @@ -52,6 +58,7 @@ private UpdateItemEnhancedRequest(Builder builder) { this.item = builder.item; this.ignoreNulls = builder.ignoreNulls; this.conditionExpression = builder.conditionExpression; + this.updateExpression = builder.updateExpression; this.returnConsumedCapacity = builder.returnConsumedCapacity; this.returnItemCollectionMetrics = builder.returnItemCollectionMetrics; } @@ -74,6 +81,7 @@ public Builder toBuilder() { return new Builder().item(item) .ignoreNulls(ignoreNulls) .conditionExpression(conditionExpression) + .updateExpression(updateExpression) .returnConsumedCapacity(returnConsumedCapacity) .returnItemCollectionMetrics(returnItemCollectionMetrics); } @@ -99,6 +107,13 @@ public Expression conditionExpression() { return conditionExpression; } + /** + * Returns the update expression {@link UpdateExpression} set on this request object, or null if it doesn't exist. + */ + public UpdateExpression updateExpression() { + return updateExpression; + } + /** * Whether to return the capacity consumed by this operation. * @@ -150,6 +165,7 @@ public boolean equals(Object o) { return Objects.equals(item, that.item) && Objects.equals(ignoreNulls, that.ignoreNulls) && Objects.equals(conditionExpression, that.conditionExpression) + && Objects.equals(updateExpression, that.updateExpression) && Objects.equals(returnConsumedCapacity, that.returnConsumedCapacity) && Objects.equals(returnItemCollectionMetrics, that.returnItemCollectionMetrics); } @@ -159,6 +175,7 @@ public int hashCode() { int result = item != null ? item.hashCode() : 0; result = 31 * result + (ignoreNulls != null ? ignoreNulls.hashCode() : 0); result = 31 * result + (conditionExpression != null ? conditionExpression.hashCode() : 0); + result = 31 * result + (updateExpression != null ? updateExpression.hashCode() : 0); result = 31 * result + (returnConsumedCapacity != null ? returnConsumedCapacity.hashCode() : 0); result = 31 * result + (returnItemCollectionMetrics != null ? returnItemCollectionMetrics.hashCode() : 0); return result; @@ -174,6 +191,7 @@ public static final class Builder { private T item; private Boolean ignoreNulls; private Expression conditionExpression; + private UpdateExpression updateExpression; private String returnConsumedCapacity; private String returnItemCollectionMetrics; @@ -220,6 +238,35 @@ public Builder item(T item) { return this; } + /** + * Define an {@link UpdateExpression} to control updating specific parts of the item in DynamoDb. The update expression + * corresponds to the DynamoDb update expression format. It can be used to set, modify and delete attributes for + * use cases that simply supplying the item does not cover; in particular, manipulating composed attributes such as + * sets or lists: + *

    + *
  • Add/remove elements to/from list attributes
  • + *
  • Add/remove elements to/from set attributes
  • + *
  • Unset or nullify attributes without modifying the whole attribute
  • + *
+ *

+ * This method will throw an exception if the expression references an attribute that is already present on the + * item, or is modified through an extension. + *

+ * Note: This is a powerful mechanism that bypasses many of the abstractions and + * safety checks in the enhanced client, and should be used with caution. Only use it when submitting only + * a configured item bean/object is insufficient. + *

+ * See {@link UpdateExpression}, {@link AddAction}, {@link DeleteAction}, {@link SetAction} and + * {@link RemoveAction} for syntax and examples. + * + * @param updateExpression a composed expression of type {@link UpdateExpression} + * @return a builder of this type + */ + public Builder updateExpression(UpdateExpression updateExpression) { + this.updateExpression = updateExpression; + return this; + } + /** * Whether to return the capacity consumed by this operation. * diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpression.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpression.java index 0e449b01ae47..20056742cff1 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpression.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpression.java @@ -140,6 +140,10 @@ public int hashCode() { return result; } + public boolean isEmpty() { + return removeActions().isEmpty() && setActions().isEmpty() && deleteActions.isEmpty() && addActions.isEmpty(); + } + /** * A builder for {@link UpdateExpression} */ diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateExpressionRequestTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateExpressionRequestTest.java new file mode 100644 index 000000000000..de5cad4ccdb5 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateExpressionRequestTest.java @@ -0,0 +1,330 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.update.AddAction; +import software.amazon.awssdk.enhanced.dynamodb.update.DeleteAction; +import software.amazon.awssdk.enhanced.dynamodb.update.RemoveAction; +import software.amazon.awssdk.enhanced.dynamodb.update.SetAction; +import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class UpdateExpressionRequestTest extends LocalDynamoDbSyncTestBase { + + private static final Set SET_ATTRIBUTE_INIT_VALUE = Stream.of("YELLOW", "BLUE", "RED", "GREEN") + .collect(Collectors.toSet()); + private static final Set SET_ATTRIBUTE_DELETE = Stream.of("YELLOW", "RED").collect(Collectors.toSet()); + private static final List REQUEST_ATTRIBUTE_LIST_INIT_VAL = Arrays.asList("a", "c"); + + private static final TableSchema TABLE_SCHEMA = + StaticTableSchema.builder(Record.class) + .newItemSupplier(Record::new) + .addAttribute(String.class, a -> a.name("id") + .getter(Record::getId) + .setter(Record::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("sort") + .getter(Record::getSort) + .setter(Record::setSort) + .tags(primarySortKey())) + .addAttribute(EnhancedType.listOf(String.class), a -> a.name("listAttribute") + .getter(Record::getListAttribute) + .setter(Record::setListAttribute)) + .addAttribute(EnhancedType.mapOf(String.class, String.class), a -> a.name("mapAttribute") + .getter(Record::getMapAttribute) + .setter(Record::setMapAttribute)) + .addAttribute(EnhancedType.setOf(String.class), a -> a.name("setAttribute") + .getter(Record::getSetAttribute) + .setter(Record::setSetAttribute)) + .build(); + + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .build(); + private final DynamoDbTable mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + + @Before + public void initRecord() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + Map map = new HashMap<>(); + map.put("a", "1"); + map.put("b", "2"); + Set sett = new HashSet<>(); + sett.add("aba"); + Record initialRecord = new Record().setId("1") + .setSort("a") + .setListAttribute(Arrays.asList("gi", "li")) + .setMapAttribute(map) + .setSetAttribute(sett); + mappedTable.putItem(initialRecord); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("table-name"))); + } + + @Test + public void listOperations() { + String partitionKey = "1"; + String sortKey = "a"; + Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build(); + + Record persistedRecord = mappedTable.getItem(key); + List listAttribute = persistedRecord.getListAttribute(); + assertThat(listAttribute).hasSize(2); + assertThat(listAttribute).containsExactly("gi", "li"); + + Record record = new Record().setId(partitionKey).setSort(sortKey); + UpdateExpression updateExpression = UpdateExpression.builder() + .addAction(setListElement("listAttribute", 0, "gah")) + .build(); + mappedTable.updateItem(r -> r.item(record).updateExpression(updateExpression)); + + persistedRecord = mappedTable.getItem(key); + listAttribute = persistedRecord.getListAttribute(); + assertThat(listAttribute).hasSize(2); + assertThat(listAttribute).containsExactly("gah", "li"); + + mappedTable.updateItem(r -> r.item(record) + .updateExpression(UpdateExpression.builder() + .addAction(removeAttributeFromList("listAttribute", 0)) + .build())); + persistedRecord = mappedTable.getItem(key); + listAttribute = persistedRecord.getListAttribute(); + assertThat(listAttribute).hasSize(1); + assertThat(listAttribute).containsExactly("li"); + + List subList = Arrays.asList("si", "fu"); + mappedTable.updateItem(r -> r.item(record) + .updateExpression(UpdateExpression.builder() + .addAction(appendToList("listAttribute", subList)) + .build())); + persistedRecord = mappedTable.getItem(key); + listAttribute = persistedRecord.getListAttribute(); + assertThat(listAttribute).hasSize(3); + assertThat(listAttribute).containsExactly("li", "si", "fu"); + } + + @Test + public void setOperations() { + String partitionKey = "1"; + String sortKey = "a"; + Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build(); + + Record persistedRecord = mappedTable.getItem(key); + Set setAttribute = persistedRecord.getSetAttribute(); + assertThat(setAttribute).hasSize(1); + assertThat(setAttribute).containsExactly("aba"); + + Record record = new Record().setId(partitionKey).setSort(sortKey); + UpdateExpression updateExpression = UpdateExpression.builder() + .addAction(addValuesToSet("setAttribute", Arrays.asList("mip", "foo"))) + .build(); + mappedTable.updateItem(r -> r.item(record).updateExpression(updateExpression)); + + persistedRecord = mappedTable.getItem(key); + setAttribute = persistedRecord.getSetAttribute(); + assertThat(setAttribute).hasSize(3); + assertThat(setAttribute).containsExactlyInAnyOrder("aba", "mip", "foo"); + + mappedTable.updateItem(r -> r.item(record) + .updateExpression(UpdateExpression.builder() + .addAction(deleteValuesFromSet("setAttribute", Arrays.asList( + "aba"))) + .build())); + persistedRecord = mappedTable.getItem(key); + setAttribute = persistedRecord.getSetAttribute(); + assertThat(setAttribute).hasSize(2); + assertThat(setAttribute).containsExactlyInAnyOrder("mip", "foo"); + } + + @Test + public void mapOperations() { + String partitionKey = "1"; + String sortKey = "a"; + Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build(); + + Record persistedRecord = mappedTable.getItem(key); + Map mapAttribute = persistedRecord.getMapAttribute(); + assertThat(mapAttribute).hasSize(2); + + Record record = new Record().setId(partitionKey).setSort(sortKey); + UpdateExpression updateExpression = UpdateExpression.builder() + .addAction(setMapAttributeToStringValue("mapAttribute", "c", + "3")) + .build(); + mappedTable.updateItem(r -> r.item(record).updateExpression(updateExpression)); + + persistedRecord = mappedTable.getItem(key); + mapAttribute = persistedRecord.getMapAttribute(); + assertThat(mapAttribute).hasSize(3); + assertThat(mapAttribute).containsEntry("c", "3"); + + mappedTable.updateItem(r -> r.item(record) + .updateExpression(UpdateExpression.builder() + .addAction(removeAttributeFromMap("mapAttribute", "a")) + .build())); + persistedRecord = mappedTable.getItem(key); + mapAttribute = persistedRecord.getMapAttribute(); + assertThat(mapAttribute).hasSize(2); + assertThat(mapAttribute).doesNotContainEntry("1", "a"); + } + + private SetAction setListElement(String listAttributeName, int index, String value) { + return SetAction.builder() + .path(keyRef(listAttributeName) + "[" + index + "]") + .value(valueRef(listAttributeName)) + .putExpressionValue(valueRef(listAttributeName), AttributeValue.fromS(value)) + .putExpressionName(keyRef(listAttributeName), listAttributeName) + .build(); + } + + private SetAction setMapAttributeToStringValue(String mapAttributeName, + String nestedAttributeName, + String value) { + return SetAction.builder() + .path(keyRef(mapAttributeName) + "." + keyRef(nestedAttributeName)) + .value(valueRef(mapAttributeName)) + .putExpressionValue(valueRef(mapAttributeName), AttributeValue.fromS(value)) + .putExpressionName(keyRef(mapAttributeName), mapAttributeName) + .putExpressionName(keyRef(nestedAttributeName), nestedAttributeName) + .build(); + } + + private SetAction appendToList(String listAttributeName, List listToAppend) { + List listValues = listToAppend.stream().map(AttributeValue::fromS).collect(Collectors.toList()); + return SetAction.builder() + .path(keyRef(listAttributeName)) + .value("list_append(" + keyRef(listAttributeName) + "," + valueRef(listAttributeName) + ")") + .putExpressionValue(valueRef(listAttributeName), AttributeValue.fromL(listValues)) + .putExpressionName(keyRef(listAttributeName), listAttributeName) + .build(); + } + + private RemoveAction removeAttributeFromList(String listAttributeName, int index) { + return RemoveAction.builder() + .path(keyRef(listAttributeName) + "[" + index + "]") + .putExpressionName(keyRef(listAttributeName), listAttributeName) + .build(); + } + + private RemoveAction removeAttributeFromMap(String mapAttributeName, String nestedAttributeName) { + return RemoveAction.builder() + .path(keyRef(mapAttributeName) + "." + keyRef(nestedAttributeName)) + .putExpressionName(keyRef(mapAttributeName), mapAttributeName) + .putExpressionName(keyRef(nestedAttributeName), nestedAttributeName) + .build(); + } + + private AddAction addValuesToSet(String setAttributeName, List values) { + return AddAction.builder() + .path(keyRef(setAttributeName)) + .value(valueRef(setAttributeName)) + .putExpressionValue(valueRef(setAttributeName), AttributeValue.fromSs(values)) + .putExpressionName(keyRef(setAttributeName), setAttributeName) + .build(); + } + + private DeleteAction deleteValuesFromSet(String setAttributeName, List values) { + return DeleteAction.builder() + .path(keyRef(setAttributeName)) + .value(valueRef(setAttributeName)) + .putExpressionValue(valueRef(setAttributeName), AttributeValue.fromSs(values)) + .putExpressionName(keyRef(setAttributeName), setAttributeName) + .build(); + } + + private static class Record { + private String id; + private String sort; + private List listAttribute; + private Map mapAttribute; + private Set setAttribute; + + private String getId() { + return id; + } + + private Record setId(String id) { + this.id = id; + return this; + } + + private String getSort() { + return sort; + } + + private Record setSort(String sort) { + this.sort = sort; + return this; + } + + private List getListAttribute() { + return listAttribute; + } + + private Record setListAttribute(List listAttribute) { + this.listAttribute = listAttribute; + return this; + } + + private Map getMapAttribute() { + return mapAttribute; + } + + private Record setMapAttribute(Map mapAttribute) { + this.mapAttribute = mapAttribute; + return this; + } + + private Set getSetAttribute() { + return setAttribute; + } + + private Record setSetAttribute(Set setAttribute) { + this.setAttribute = setAttribute; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record record = (Record) o; + return Objects.equals(id, record.id) && + Objects.equals(sort, record.sort) && + Objects.equals(listAttribute, record.listAttribute) && + Objects.equals(mapAttribute, record.mapAttribute) && + Objects.equals(setAttribute, record.setAttribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, listAttribute, mapAttribute, setAttribute); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateExpressionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateExpressionTest.java index e2271f424d3b..a7eba76f007d 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateExpressionTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateExpressionTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef; import java.util.Arrays; import java.util.List; @@ -19,10 +21,9 @@ import software.amazon.awssdk.enhanced.dynamodb.extensions.WriteModification; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordForUpdateExpressions; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.OperationName; -import software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation; -import software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionConverter; import software.amazon.awssdk.enhanced.dynamodb.update.AddAction; import software.amazon.awssdk.enhanced.dynamodb.update.DeleteAction; +import software.amazon.awssdk.enhanced.dynamodb.update.SetAction; import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; @@ -33,16 +34,15 @@ public class UpdateExpressionTest extends LocalDynamoDbSyncTestBase { private static final Set SET_ATTRIBUTE_INIT_VALUE = Stream.of("YELLOW", "BLUE", "RED", "GREEN") .collect(Collectors.toSet()); private static final Set SET_ATTRIBUTE_DELETE = Stream.of("YELLOW", "RED").collect(Collectors.toSet()); - private static final String NUMBER_ATTRIBUTE_REF = "extensionNumberAttribute"; private static final long NUMBER_ATTRIBUTE_VALUE = 5L; private static final String NUMBER_ATTRIBUTE_VALUE_REF = ":increment_value_ref"; private static final String SET_ATTRIBUTE_REF = "extensionSetAttribute"; - + private static final List REQUEST_ATTRIBUTE_LIST_INIT_VAL = Arrays.asList("a", "c"); private static final TableSchema TABLE_SCHEMA = TableSchema.fromClass(RecordForUpdateExpressions.class); private DynamoDbTable mappedTable; - private void initClientWithExtensions(DynamoDbEnhancedClientExtension... extensions) { + private void initClientWithExtensionList(DynamoDbEnhancedClientExtension... extensions) { DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(getDynamoDbClient()) .extensions(extensions) @@ -59,8 +59,8 @@ public void deleteTable() { @Test public void attribute_notInPojo_notFilteredInExtension_ignoresNulls_updatesNormally() { - initClientWithExtensions(new ItemPreservingUpdateExtension()); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemPreservingUpdateExtension()); + RecordForUpdateExpressions record = createBasicRecord("1"); mappedTable.updateItem(r -> r.item(record).ignoreNulls(true)); @@ -73,17 +73,11 @@ public void attribute_notInPojo_notFilteredInExtension_ignoresNulls_updatesNorma * This test case represents the most likely extension UpdateExpression use case; * an attribute is set in the extensions and isn't present in the request POJO item, and there is no change in * the request to set ignoreNull to true. - *

- * By default, ignorNull is false, so attributes that aren't set on the request are deleted from the DDB table through - * the updateItemOperation generating REMOVE actions for those attributes. This is prevented by - * {@link UpdateItemOperation} using {@link UpdateExpressionConverter#findAttributeNames(UpdateExpression)} - * to not create REMOVE actions attributes it finds referenced in an extension UpdateExpression. - * Therefore, this use case updates normally. */ @Test public void attribute_notInPojo_notFilteredInExtension_defaultSetsNull_updatesNormally() { - initClientWithExtensions(new ItemPreservingUpdateExtension()); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemPreservingUpdateExtension()); + RecordForUpdateExpressions record = createBasicRecord("1"); mappedTable.updateItem(r -> r.item(record)); @@ -92,10 +86,69 @@ public void attribute_notInPojo_notFilteredInExtension_defaultSetsNull_updatesNo assertThat(persistedRecord.getExtensionNumberAttribute()).isEqualTo(5L); } + @Test + public void attribute_addedToRequestExpression_noIgnoreNull_updatesNormally() { + initClientWithExtensionList(); + RecordForUpdateExpressions initialRecord = createBasicRecord("1"); + putInitialItemAndVerify(initialRecord); + + RecordForUpdateExpressions keyRecord = createKeyOnlyRecord("1"); + mappedTable.updateItem(r -> r.item(keyRecord) + .updateExpression(expressionWithSetListElement(1, "b"))); + + RecordForUpdateExpressions persistedRecord = mappedTable.getItem(keyRecord); + List requestAttributeList = persistedRecord.getRequestAttributeList(); + assertThat(requestAttributeList).hasSize(2); + assertThat(requestAttributeList).containsExactly("a", "b"); + } + + @Test + public void attribute_addedToRequestExpression_ignoreNulls_updatesNormally() { + initClientWithExtensionList(); + RecordForUpdateExpressions initialRecord = createBasicRecord("1"); + putInitialItemAndVerify(initialRecord); + + RecordForUpdateExpressions keyRecord = createKeyOnlyRecord("1"); + mappedTable.updateItem(r -> r.item(keyRecord) + .ignoreNulls(true) + .updateExpression(expressionWithSetListElement(1, "b"))); + + RecordForUpdateExpressions persistedRecord = mappedTable.getItem(keyRecord); + List requestAttributeList = persistedRecord.getRequestAttributeList(); + assertThat(requestAttributeList).hasSize(2); + assertThat(requestAttributeList).containsExactly("a", "b"); + } + + @Test + public void attribute_inPojo_addedToRequestExpression_ddbError() { + initClientWithExtensionList(); + RecordForUpdateExpressions initialRecord = createBasicRecord("1"); + putInitialItemAndVerify(initialRecord); + + RecordForUpdateExpressions updateRecord = createKeyOnlyRecord("1"); + updateRecord.setRequestAttributeList(Arrays.asList("A")); + assertThatThrownBy(() -> mappedTable.updateItem(r -> r.item(updateRecord) + .updateExpression(expressionWithSetListElement(1, "b")))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Attempt to coalesce two expressions with conflicting expression values") + .hasMessageContaining("requestAttributeList"); + } + + @Test + public void attribute_inExtension_addedToRequestExpression_ddbError() { + initClientWithExtensionList(new ItemPreservingUpdateExtension()); + RecordForUpdateExpressions record = createBasicRecord("1"); + assertThatThrownBy(() -> mappedTable.updateItem(r -> r.item(record) + .updateExpression(expressionWithSetExtensionAttribute()))) + .isInstanceOf(DynamoDbException.class) + .hasMessageContaining("Two document paths") + .hasMessageContaining(NUMBER_ATTRIBUTE_REF); + } + @Test public void attribute_notInPojo_filteredInExtension_ignoresNulls_updatesNormally() { - initClientWithExtensions(new ItemFilteringUpdateExtension()); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemFilteringUpdateExtension()); + RecordForUpdateExpressions record = createBasicRecord("1"); record.setExtensionSetAttribute(SET_ATTRIBUTE_INIT_VALUE); mappedTable.putItem(record); @@ -107,8 +160,8 @@ public void attribute_notInPojo_filteredInExtension_ignoresNulls_updatesNormally @Test public void attribute_notInPojo_filteredInExtension_defaultSetsNull_updatesNormally() { - initClientWithExtensions(new ItemFilteringUpdateExtension()); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemFilteringUpdateExtension()); + RecordForUpdateExpressions record = createBasicRecord("1"); record.setExtensionSetAttribute(SET_ATTRIBUTE_INIT_VALUE); mappedTable.putItem(record); @@ -124,8 +177,8 @@ public void attribute_notInPojo_filteredInExtension_defaultSetsNull_updatesNorma */ @Test public void attribute_inPojo_notFilteredInExtension_ignoresNulls_ddbError() { - initClientWithExtensions(new ItemPreservingUpdateExtension()); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemPreservingUpdateExtension()); + RecordForUpdateExpressions record = createBasicRecord("1"); record.setExtensionNumberAttribute(100L); verifyDDBError(record, true); @@ -133,8 +186,8 @@ public void attribute_inPojo_notFilteredInExtension_ignoresNulls_ddbError() { @Test public void attribute_inPojo_notFilteredInExtension_defaultSetsNull_ddbError() { - initClientWithExtensions(new ItemPreservingUpdateExtension()); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemPreservingUpdateExtension()); + RecordForUpdateExpressions record = createBasicRecord("1"); record.setExtensionNumberAttribute(100L); verifyDDBError(record, false); @@ -147,8 +200,8 @@ public void attribute_inPojo_notFilteredInExtension_defaultSetsNull_ddbError() { */ @Test public void attribute_inPojo_filteredInExtension_ignoresNulls_updatesNormally() { - initClientWithExtensions(new ItemFilteringUpdateExtension()); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemFilteringUpdateExtension()); + RecordForUpdateExpressions record = createBasicRecord("1"); record.setExtensionSetAttribute(SET_ATTRIBUTE_INIT_VALUE); mappedTable.putItem(record); @@ -161,8 +214,8 @@ public void attribute_inPojo_filteredInExtension_ignoresNulls_updatesNormally() @Test public void attribute_inPojo_filteredInExtension_defaultSetsNull_updatesNormally() { - initClientWithExtensions(new ItemFilteringUpdateExtension()); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemFilteringUpdateExtension()); + RecordForUpdateExpressions record = createBasicRecord("1"); record.setExtensionSetAttribute(SET_ATTRIBUTE_INIT_VALUE); mappedTable.putItem(record); @@ -175,13 +228,13 @@ public void attribute_inPojo_filteredInExtension_defaultSetsNull_updatesNormally @Test public void chainedExtensions_noDuplicates_ignoresNulls_updatesNormally() { - initClientWithExtensions(new ItemPreservingUpdateExtension(), new ItemFilteringUpdateExtension()); - RecordForUpdateExpressions putRecord = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemPreservingUpdateExtension(), new ItemFilteringUpdateExtension()); + RecordForUpdateExpressions putRecord = createBasicRecord("1"); putRecord.setExtensionNumberAttribute(11L); putRecord.setExtensionSetAttribute(SET_ATTRIBUTE_INIT_VALUE); mappedTable.putItem(putRecord); - RecordForUpdateExpressions updateRecord = createRecordWithoutExtensionAttributes(); + RecordForUpdateExpressions updateRecord = createBasicRecord("1"); updateRecord.setStringAttribute("updated"); mappedTable.updateItem(r -> r.item(updateRecord).ignoreNulls(true)); @@ -193,26 +246,26 @@ public void chainedExtensions_noDuplicates_ignoresNulls_updatesNormally() { @Test public void chainedExtensions_duplicateAttributes_sameValue_sameValueRef_ddbError() { - initClientWithExtensions(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension()); - verifyDDBError(createRecordWithoutExtensionAttributes(), false); + initClientWithExtensionList(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension()); + verifyDDBError(createBasicRecord("1"), false); } @Test public void chainedExtensions_duplicateAttributes_sameValue_differentValueRef_ddbError() { - initClientWithExtensions(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension(NUMBER_ATTRIBUTE_VALUE, ":ref")); - verifyDDBError(createRecordWithoutExtensionAttributes(), false); + initClientWithExtensionList(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension(NUMBER_ATTRIBUTE_VALUE, ":ref")); + verifyDDBError(createBasicRecord("1"), false); } @Test public void chainedExtensions_duplicateAttributes_differentValue_differentValueRef_ddbError() { - initClientWithExtensions(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension(13L, ":ref")); - verifyDDBError(createRecordWithoutExtensionAttributes(), false); + initClientWithExtensionList(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension(13L, ":ref")); + verifyDDBError(createBasicRecord("1"), false); } @Test public void chainedExtensions_duplicateAttributes_differentValue_sameValueRef_operationMergeError() { - initClientWithExtensions(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension(10L, NUMBER_ATTRIBUTE_VALUE_REF)); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension(10L, NUMBER_ATTRIBUTE_VALUE_REF)); + RecordForUpdateExpressions record = createBasicRecord("1"); assertThatThrownBy(() ->mappedTable.updateItem(r -> r.item(record))) .isInstanceOf(IllegalArgumentException.class) @@ -222,8 +275,8 @@ public void chainedExtensions_duplicateAttributes_differentValue_sameValueRef_op @Test public void chainedExtensions_duplicateAttributes_invalidValueRef_operationMergeError() { - initClientWithExtensions(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension(10L, "illegal")); - RecordForUpdateExpressions record = createRecordWithoutExtensionAttributes(); + initClientWithExtensionList(new ItemPreservingUpdateExtension(), new ItemPreservingUpdateExtension(10L, "illegal")); + RecordForUpdateExpressions record = createBasicRecord("1"); assertThatThrownBy(() ->mappedTable.updateItem(r -> r.item(record))) .isInstanceOf(DynamoDbException.class) @@ -246,13 +299,52 @@ private void verifySetAttribute(RecordForUpdateExpressions record) { assertThat(persistedRecord.getExtensionSetAttribute()).isEqualTo(expectedAttribute); } - private RecordForUpdateExpressions createRecordWithoutExtensionAttributes() { + private RecordForUpdateExpressions createKeyOnlyRecord(String id) { + RecordForUpdateExpressions record = new RecordForUpdateExpressions(); + record.setId(id); + return record; + } + + private RecordForUpdateExpressions createBasicRecord(String id) { RecordForUpdateExpressions record = new RecordForUpdateExpressions(); - record.setId("1"); + record.setId(id); record.setStringAttribute("init"); + record.setRequestAttributeList(REQUEST_ATTRIBUTE_LIST_INIT_VAL); return record; } + private void putInitialItemAndVerify(RecordForUpdateExpressions record) { + mappedTable.putItem(r -> r.item(record)); + RecordForUpdateExpressions persistedRecord = mappedTable.getItem(record); + List requestAttributeList = persistedRecord.getRequestAttributeList(); + assertThat(requestAttributeList).hasSize(2); + assertThat(requestAttributeList).isEqualTo(REQUEST_ATTRIBUTE_LIST_INIT_VAL); + } + + private UpdateExpression expressionWithSetListElement(int index, String value) { + String listAttributeName = "requestAttributeList"; + AttributeValue listElementValue = AttributeValue.builder().s(value).build(); + SetAction setListElement = SetAction.builder() + .path(keyRef(listAttributeName) + "[" + index + "]") + .value(valueRef(listAttributeName)) + .putExpressionValue(valueRef(listAttributeName), listElementValue) + .putExpressionName(keyRef(listAttributeName), listAttributeName) + .build(); + return UpdateExpression.builder().addAction(setListElement).build(); + } + + private UpdateExpression expressionWithSetExtensionAttribute() { + String attributeName = "extensionNumberAttribute"; + AttributeValue elementValue = AttributeValue.builder().n("11").build(); + SetAction setAttribute = SetAction.builder() + .path(keyRef(attributeName)) + .value(valueRef(attributeName)) + .putExpressionValue(valueRef(attributeName), elementValue) + .putExpressionName(keyRef(attributeName), attributeName) + .build(); + return UpdateExpression.builder().addAction(setAttribute).build(); + } + private static final class ItemPreservingUpdateExtension implements DynamoDbEnhancedClientExtension { private long incrementValue; private String valueRef; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordForUpdateExpressions.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordForUpdateExpressions.java index 2e2c89c8a265..241163d3e274 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordForUpdateExpressions.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordForUpdateExpressions.java @@ -17,6 +17,7 @@ import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS; +import java.util.List; import java.util.Set; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; @@ -26,6 +27,7 @@ public class RecordForUpdateExpressions { private String id; private String stringAttribute1; + private List requestAttributeList; private Long extensionAttribute1; private Set extensionAttribute2; @@ -47,6 +49,14 @@ public void setStringAttribute(String stringAttribute1) { this.stringAttribute1 = stringAttribute1; } + public List getRequestAttributeList() { + return requestAttributeList; + } + + public void setRequestAttributeList(List stringRequestAttribute) { + this.requestAttributeList = stringRequestAttribute; + } + public Long getExtensionNumberAttribute() { return extensionAttribute1; } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverterTest.java index e3d389fd5261..44b1f90794d6 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverterTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverterTest.java @@ -51,9 +51,7 @@ void convert_emptyExpression() { UpdateExpression updateExpression = UpdateExpression.builder().build(); Expression expression = UpdateExpressionConverter.toExpression(updateExpression); - assertThat(expression.expression()).isEmpty(); - assertThat(expression.expressionNames()).isEmpty(); - assertThat(expression.expressionValues()).isEmpty(); + assertThat(expression).isNull(); } @Test