Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Amazon DynamoDB Enhanced Client",
"contributor": "",
"description": "Support update expressions in single request update"
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@

import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
Expand All @@ -36,8 +34,10 @@
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.IgnoreNullsMode;
import software.amazon.awssdk.enhanced.dynamodb.model.TransactUpdateItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateExpressionMergeStrategy;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression;
Expand Down Expand Up @@ -132,7 +132,7 @@ public UpdateItemRequest generateRequest(TableSchema<T> tableSchema,
Map<String, AttributeValue> keyAttributes = filterMap(itemMap, entry -> primaryKeys.contains(entry.getKey()));
Map<String, AttributeValue> nonKeyAttributes = filterMap(itemMap, entry -> !primaryKeys.contains(entry.getKey()));

Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, nonKeyAttributes);
Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, nonKeyAttributes, request);
Expression conditionExpression = generateConditionExpressionIfExist(transformation, request);

Map<String, String> expressionNames = coalesceExpressionNames(updateExpression, conditionExpression);
Expand Down Expand Up @@ -271,27 +271,37 @@ public TransactWriteItem generateTransactWriteItem(TableSchema<T> tableSchema, O
}

/**
* Retrieves the UpdateExpression from extensions if existing, and then creates an UpdateExpression for the request POJO
* 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.
* Combines POJO, extension, and request update expressions via {@link UpdateExpressionResolver}, honoring the request's
* {@link UpdateExpressionMergeStrategy}. For {@link UpdateExpressionMergeStrategy#PRIORITIZE_HIGHER_SOURCE}, see
* {@link UpdateExpressionMergeStrategy} (path overlap resolution; request wins over extension over POJO).
*/
private Expression generateUpdateExpressionIfExist(TableMetadata tableMetadata,
WriteModification transformation,
Map<String, AttributeValue> attributes) {
UpdateExpression updateExpression = null;
if (transformation != null && transformation.updateExpression() != null) {
updateExpression = transformation.updateExpression();
}
if (!attributes.isEmpty()) {
List<String> 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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UpdateItemOperationTest can be updated as part of this PR to add tests for the new updateExpression and updateExpressionMergeStrategy fields flowing through generateRequest()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UpdateItemOperationTest and UpdateItemOperationTransactTest were updated with the following scenarios:

Scenario Result
Request only Only the request update expression.
Extension + request, different paths (LEGACY or PRIORITIZE with no overlap) The full extension expression and the full request expression—every action from each, on its own paths.
Extension + request, same path (PRIORITIZE_HIGHER_SOURCE) The request side for that path; extension actions that hit the same path are dropped.
Extension + request, same path (LEGACY) Both extension and request actions on that path, all chained together.
POJO + request (LEGACY) Everything from the item map plus everything from the request expression.
POJO + extension (LEGACY) Everything from the item map plus everything from the extension expression.
POJO + extension + request (LEGACY) Item, extension, and request contributions, all merged.
POJO + request (PRIORITIZE_HIGHER_SOURCE, same attribute in both) Request wins on the shared attribute; the item still supplies updates for attributes the request does not take over.
POJO + extension (PRIORITIZE_HIGHER_SOURCE, same attribute in both) Extension wins on the shared attribute; the item still supplies updates for attributes the extension does not take over.
POJO + extension + request (PRIORITIZE_HIGHER_SOURCE) Request actions first; then extension actions that do not collide with the request; then item actions that do not collide with either.
Transact (same setups) Same rules; the merged result is what appears on both the plain UpdateItemRequest and the transact Update payload.

TableMetadata tableMetadata,
WriteModification transformation,
Map<String, AttributeValue> attributes,
Either<UpdateItemEnhancedRequest<T>, TransactUpdateItemEnhancedRequest<T>> request) {

UpdateExpression requestUpdateExpression =
request.left().map(UpdateItemEnhancedRequest::updateExpression)
.orElseGet(() -> request.right().map(TransactUpdateItemEnhancedRequest::updateExpression).orElse(null));

UpdateExpressionMergeStrategy updateExpressionMergeStrategy =
request.left().map(UpdateItemEnhancedRequest::updateExpressionMergeStrategy)
.orElseGet(() -> request.right()
.map(TransactUpdateItemEnhancedRequest::updateExpressionMergeStrategy)
.orElse(UpdateExpressionMergeStrategy.LEGACY));

UpdateExpressionResolver updateExpressionResolver =
UpdateExpressionResolver.builder()
.tableMetadata(tableMetadata)
.nonKeyAttributes(attributes)
.requestExpression(requestUpdateExpression)
.updateExpressionMergeStrategy(updateExpressionMergeStrategy)
.extensionExpression(transformation != null ? transformation.updateExpression() : null)
.build();

UpdateExpression mergedUpdateExpression = updateExpressionResolver.resolve();
return UpdateExpressionConverter.toExpression(mergedUpdateExpression);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,14 @@ 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
* Attempts to find the distinct 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
* attribute references and list references, the leftmost part will be returned if composition is detected.
* <p>
* Examples: The expression contains a {@link DeleteAction} with a path value of 'MyAttribute[1]'; the list returned
* will have 'MyAttribute' as an element.}
*
* @return A list of top level attribute names that have update actions associated.
* @return A list of distinct top-level attribute names that have update actions associated.
*/
public static List<String> findAttributeNames(UpdateExpression updateExpression) {
if (updateExpression == null) {
Expand All @@ -109,7 +109,7 @@ public static List<String> findAttributeNames(UpdateExpression updateExpression)
List<String> attributeNames = listPathsWithoutTokens(updateExpression);
List<String> attributeNamesFromTokens = listAttributeNamesFromTokens(updateExpression);
attributeNames.addAll(attributeNamesFromTokens);
return attributeNames;
return attributeNames.stream().distinct().collect(Collectors.toList());
}

private static List<String> groupExpressions(UpdateExpression expression) {
Expand Down
Loading