Skip to content

Commit f751879

Browse files
committed
DynamoDb enhanced client: support UpdateExpressions in single-request update
1 parent 243de4d commit f751879

File tree

5 files changed

+90
-72
lines changed

5 files changed

+90
-72
lines changed

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public UpdateItemRequest generateRequest(TableSchema<T> tableSchema,
131131
Map<String, AttributeValue> keyAttributes = filterMap(itemMap, entry -> primaryKeys.contains(entry.getKey()));
132132
Map<String, AttributeValue> nonKeyAttributes = filterMap(itemMap, entry -> !primaryKeys.contains(entry.getKey()));
133133

134-
Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, request, nonKeyAttributes);
134+
Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, nonKeyAttributes, request);
135135
Expression conditionExpression = generateConditionExpressionIfExist(transformation, request);
136136

137137
Map<String, String> expressionNames = coalesceExpressionNames(updateExpression, conditionExpression);
@@ -270,26 +270,46 @@ public TransactWriteItem generateTransactWriteItem(TableSchema<T> tableSchema, O
270270
}
271271

272272
/**
273-
* Retrieves the UpdateExpression from extensions if existing, and then creates an UpdateExpression for the request POJO
274-
* if there are attributes to be updated (most likely). If both exist, they are merged and the code generates a final
275-
* Expression that represent the result.
273+
* Generates the final UpdateExpression by merging expressions from multiple sources:
274+
* <ol>
275+
* <li><b>POJO attributes</b>: Creates SET/REMOVE actions for non-key attributes from the item POJO</li>
276+
* <li><b>Extension expressions</b>: Includes UpdateExpression from extensions (if any)</li>
277+
* <li><b>Request expressions</b>: Includes explicit UpdateExpression from the request (if any)</li>
278+
* </ol>
279+
*
280+
* <p><b>Conflict Detection:</b>
281+
* <ul>
282+
* <li>POJO vs Request conflicts are detected client-side and throw {@code IllegalArgumentException}</li>
283+
* <li>Extension vs Request conflicts are detected server-side by DynamoDB and throw {@code DynamoDbException}</li>
284+
* </ul>
285+
*
286+
* <p><b>Attribute Filtering:</b>
287+
* Attributes referenced in extension expressions are automatically excluded from REMOVE actions
288+
* to prevent conflicts, even when {@code ignoreNulls} is false.
289+
*
290+
* @param tableMetadata metadata about the table structure
291+
* @param transformation write modification from extensions containing UpdateExpression
292+
* @param attributes non-key attributes from the POJO item
293+
* @param request the update request containing optional explicit UpdateExpression
294+
* @return merged Expression containing the final update expression, or null if no updates needed
276295
*/
277296
private Expression generateUpdateExpressionIfExist(
278297
TableMetadata tableMetadata,
279298
WriteModification transformation,
280-
Either<UpdateItemEnhancedRequest<T>, TransactUpdateItemEnhancedRequest<T>> request,
281-
Map<String, AttributeValue> nonKeyAttributes) {
299+
Map<String, AttributeValue> attributes,
300+
Either<UpdateItemEnhancedRequest<T>, TransactUpdateItemEnhancedRequest<T>> request) {
282301

283-
UpdateExpression requestUpdateExpression = request.map(r -> Optional.ofNullable(r.updateExpression()),
284-
r -> Optional.ofNullable(r.updateExpression()))
285-
.orElse(null);
302+
UpdateExpression requestUpdateExpression =
303+
request.map(r -> Optional.ofNullable(r.updateExpression()),
304+
r -> Optional.ofNullable(r.updateExpression()))
305+
.orElse(null);
286306

287307
UpdateExpressionResolver updateExpressionResolver =
288308
UpdateExpressionResolver.builder()
289309
.tableMetadata(tableMetadata)
290-
.itemNonKeyAttributes(nonKeyAttributes)
310+
.itemNonKeyAttributes(attributes)
291311
.requestExpression(requestUpdateExpression)
292-
.transformationExpression(transformation != null ? transformation.updateExpression() : null)
312+
.extensionExpression(transformation != null ? transformation.updateExpression() : null)
293313
.build();
294314

295315
UpdateExpression mergedUpdateExpression = updateExpressionResolver.resolve();

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ private UpdateExpressionConverter() {
7676
* @param expression the UpdateExpression to convert
7777
*
7878
* @return an Expression representing the concatenation of all actions in this UpdateExpression, or null if the expression
79-
* is null or empty
79+
* is null or empty (contains no actions) to avoid generating invalid empty expressions that would be rejected by DynamoDB.
8080
*/
8181
public static Expression toExpression(UpdateExpression expression) {
8282
if (expression == null || expression.isEmpty()) {

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionResolver.java

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
3131

3232
/**
33-
*
33+
* Resolves and merges UpdateExpressions from multiple sources (item attributes, extensions, requests)
34+
* with priority-based conflict resolution and smart filtering to prevent attribute conflicts.
3435
*/
3536
@SdkInternalApi
3637
public final class UpdateExpressionResolver {
@@ -41,7 +42,7 @@ public final class UpdateExpressionResolver {
4142
private final TableMetadata tableMetadata;
4243

4344
private UpdateExpressionResolver(Builder builder) {
44-
this.extensionExpression = builder.transformationExpression;
45+
this.extensionExpression = builder.extensionExpression;
4546
this.requestExpression = builder.requestExpression;
4647
this.itemNonKeyAttributes = builder.nonKeyAttributes;
4748
this.tableMetadata = builder.tableMetadata;
@@ -52,31 +53,40 @@ public static Builder builder() {
5253
}
5354

5455
/**
55-
* Resolves all available and potential update expressions by priority and returns a merged update expression. It may return
56-
* null, if the item attribute map is empty / does not contain non-null attributes and no other update expressions are
57-
* present.
58-
* <p>
59-
* Conditions that will result in error:
60-
* <ul>
61-
* <li>Two expressions contain actions referencing the same attribute</li>
62-
* </ul>
63-
* <p>
64-
* <b>Note: </b> The presence of attributes in update expressions submitted through the request or generated from extensions
65-
* take precedence over removing attributes based on item configuration.
66-
* For example, when IGNORE_NULLS is set to true (default), the client generates REMOVE actions for all
67-
* attributes in the schema that are not explicitly set in the request item submitted to the operation. If such
68-
* attributes are referenced in update expressions on the request or from extensions, the remove actions are filtered
69-
* out.
56+
* Merges UpdateExpressions from three sources in priority order:
57+
* <ol>
58+
* <li><b>Item attributes</b>: SET/REMOVE actions from item POJO (lowest priority)</li>
59+
* <li><b>Extension expressions</b>: Override conflicting item actions (medium priority)</li>
60+
* <li><b>Request expressions</b>: Override all other sources (highest priority)</li>
61+
* </ol>
62+
*
63+
* <p><b>Implementation steps:</b>
64+
* <ol>
65+
* <li>Find attributes referenced in extension and request expressions to exclude them from removal</li>
66+
* <li>Generate SET actions for all non-null item attributes</li>
67+
* <li>Generate REMOVE actions for null item attributes, excluding those referenced in expressions</li>
68+
* <li>Combine item SET and REMOVE expressions into a single item expression</li>
69+
* <li>Merge extension expressions with item expression (extension overrides conflicting item actions)</li>
70+
* <li>Merge request expressions with combined result (request overrides all conflicting actions)</li>
71+
* </ol>
72+
*
73+
* <p>Higher priority expressions win conflicts. REMOVE actions are filtered to prevent conflicts,
74+
* even when {@code ignoreNulls} is false.
75+
*
76+
* <p><b>Backward compatibility:</b> This enhancement is not a breaking change. Without request
77+
* expressions, behavior is identical to previous versions.
78+
*
79+
* @return merged UpdateExpression, or empty if no updates needed
7080
*/
7181
public UpdateExpression resolve() {
72-
UpdateExpression itemSetExpression = generateItemSetExpression(itemNonKeyAttributes, tableMetadata);
82+
List<String> excludedFromRemoval = attributesPresentInExpressions(Arrays.asList(extensionExpression, requestExpression));
7383

74-
List<String> nonRemoveAttributes = attributesPresentInExpressions(Arrays.asList(extensionExpression, requestExpression));
75-
UpdateExpression itemRemoveExpression = generateItemRemoveExpression(itemNonKeyAttributes, nonRemoveAttributes);
84+
UpdateExpression itemSetExpression = generateItemSetExpression(itemNonKeyAttributes, tableMetadata);
85+
UpdateExpression itemRemoveExpression = generateItemRemoveExpression(itemNonKeyAttributes, excludedFromRemoval);
86+
UpdateExpression itemFinalExpression = UpdateExpression.mergeExpressions(itemSetExpression, itemRemoveExpression);
7687

77-
UpdateExpression itemExpression = UpdateExpression.mergeExpressions(itemSetExpression, itemRemoveExpression);
78-
UpdateExpression extensionItemExpression = UpdateExpression.mergeExpressions(extensionExpression, itemExpression);
79-
return UpdateExpression.mergeExpressions(requestExpression, extensionItemExpression);
88+
UpdateExpression itemAndExtensionExpression = UpdateExpression.mergeExpressions(extensionExpression, itemFinalExpression);
89+
return UpdateExpression.mergeExpressions(requestExpression, itemAndExtensionExpression);
8090
}
8191

8292
private static List<String> attributesPresentInExpressions(List<UpdateExpression> updateExpressions) {
@@ -108,7 +118,7 @@ public static UpdateExpression generateItemRemoveExpression(Map<String, Attribut
108118
public static final class Builder {
109119

110120
private TableMetadata tableMetadata;
111-
private UpdateExpression transformationExpression;
121+
private UpdateExpression extensionExpression;
112122
private UpdateExpression requestExpression;
113123
private Map<String, AttributeValue> nonKeyAttributes;
114124

@@ -117,8 +127,8 @@ public Builder tableMetadata(TableMetadata tableMetadata) {
117127
return this;
118128
}
119129

120-
public Builder transformationExpression(UpdateExpression transformationExpression) {
121-
this.transformationExpression = transformationExpression;
130+
public Builder extensionExpression(UpdateExpression extensionExpression) {
131+
this.extensionExpression = extensionExpression;
122132
return this;
123133
}
124134

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/TransactUpdateItemEnhancedRequest.java

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -247,27 +247,21 @@ public Builder<T> item(T item) {
247247
}
248248

249249
/**
250-
* Define an {@link UpdateExpression} to control updating specific parts of the item in DynamoDb. The update expression
251-
* corresponds to the DynamoDb update expression format. It can be used to set, modify and delete attributes for use cases
252-
* that simply supplying the item does not cover; in particular, manipulating composed attributes such as sets or lists:
253-
* <ul>
254-
* <li>Add/remove elements to/from list attributes</li>
255-
* <li>Add/remove elements to/from set attributes</li>
256-
* <li>Unset or nullify attributes without modifying the whole attribute</li>
257-
* </ul>
250+
* Specifies custom update operations using DynamoDB's native update expression syntax. Enables advanced modifications
251+
* like incrementing counters or modifying lists/sets.
258252
* <p>
259-
* This method will throw an exception if the expression references an attribute that is already present on the
260-
* item, or is modified through an extension.
253+
* <b>Precedence:</b> Request expressions (highest) > Extension expressions > Item attributes (lowest).
254+
* This method overrides any conflicting operations from item attributes or extensions.
261255
* <p>
262-
* <b>Note: </b>This is a powerful mechanism that bypasses many of the abstractions and
263-
* safety checks in the enhanced client, and should be used with caution. Only use it when submitting only
264-
* a configured item bean/object is insufficient.
265-
* <p>
266-
* See {@link UpdateExpression}, {@link AddAction}, {@link DeleteAction}, {@link SetAction} and
267-
* {@link RemoveAction} for syntax and examples.
256+
* <b>Backward compatible:</b> New feature that doesn't affect existing behavior when not used.
268257
*
269-
* @param updateExpression a composed expression of type {@link UpdateExpression}
258+
* @param updateExpression the update operations to perform
270259
* @return a builder of this type
260+
* @see UpdateExpression
261+
* @see SetAction
262+
* @see AddAction
263+
* @see RemoveAction
264+
* @see DeleteAction
271265
*/
272266
public Builder<T> updateExpression(UpdateExpression updateExpression) {
273267
this.updateExpression = updateExpression;

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -332,27 +332,21 @@ public Builder<T> item(T item) {
332332
}
333333

334334
/**
335-
* Define an {@link UpdateExpression} to control updating specific parts of the item in DynamoDb. The update expression
336-
* corresponds to the DynamoDb update expression format. It can be used to set, modify and delete attributes for use cases
337-
* that simply supplying the item does not cover; in particular, manipulating composed attributes such as sets or lists:
338-
* <ul>
339-
* <li>Add/remove elements to/from list attributes</li>
340-
* <li>Add/remove elements to/from set attributes</li>
341-
* <li>Unset or nullify attributes without modifying the whole attribute</li>
342-
* </ul>
335+
* Specifies custom update operations using DynamoDB's native update expression syntax. Enables advanced modifications
336+
* like incrementing counters or modifying lists/sets.
343337
* <p>
344-
* This method will throw an exception if the expression references an attribute that is already present on the
345-
* item, or is modified through an extension.
338+
* <b>Precedence:</b> Request expressions (highest) > Extension expressions > Item attributes (lowest).
339+
* This method overrides any conflicting operations from item attributes or extensions.
346340
* <p>
347-
* <b>Note: </b>This is a powerful mechanism that bypasses many of the abstractions and
348-
* safety checks in the enhanced client, and should be used with caution. Only use it when submitting only
349-
* a configured item bean/object is insufficient.
350-
* <p>
351-
* See {@link UpdateExpression}, {@link AddAction}, {@link DeleteAction}, {@link SetAction} and
352-
* {@link RemoveAction} for syntax and examples.
341+
* <b>Backward compatible:</b> New feature that doesn't affect existing behavior when not used.
353342
*
354-
* @param updateExpression a composed expression of type {@link UpdateExpression}
343+
* @param updateExpression the update operations to perform
355344
* @return a builder of this type
345+
* @see UpdateExpression
346+
* @see SetAction
347+
* @see AddAction
348+
* @see RemoveAction
349+
* @see DeleteAction
356350
*/
357351
public Builder<T> updateExpression(UpdateExpression updateExpression) {
358352
this.updateExpression = updateExpression;

0 commit comments

Comments
 (0)