Skip to content

Commit 1a8be21

Browse files
committed
Support update expressions in single request update
1 parent 22a0ffa commit 1a8be21

File tree

7 files changed

+31
-73
lines changed

7 files changed

+31
-73
lines changed

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,10 @@ private UpdateExpressionConverter() {
7373
* of whether it represents an update expression, conditional expression or another type of expression, since once
7474
* the string is generated that update expression is the final format accepted by DDB.
7575
*
76-
* @param expression the UpdateExpression to convert
77-
*
78-
* @return an Expression representing the concatenation of all actions in this UpdateExpression, or null if the expression
79-
* is null or empty (contains no actions) to avoid generating invalid empty expressions that would be rejected by DynamoDB.
76+
* @return an Expression representing the concatenation of all actions in this UpdateExpression
8077
*/
8178
public static Expression toExpression(UpdateExpression expression) {
82-
if (expression == null || expression.isEmpty()) {
79+
if (expression == null) {
8380
return null;
8481
}
8582
Map<String, AttributeValue> expressionValues = mergeExpressionValues(expression);

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,27 @@
1515

1616
package software.amazon.awssdk.enhanced.dynamodb.internal.update;
1717

18+
import static java.util.Objects.requireNonNull;
1819
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
1920
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.removeActionsFor;
2021
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.setActionsFor;
2122
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;
2223

2324
import java.util.Arrays;
25+
import java.util.Collections;
2426
import java.util.List;
2527
import java.util.Map;
28+
import java.util.Objects;
29+
import java.util.Set;
2630
import java.util.stream.Collectors;
2731
import software.amazon.awssdk.annotations.SdkInternalApi;
2832
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
2933
import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression;
3034
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
3135

3236
/**
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.
37+
* Resolves and merges UpdateExpressions from multiple sources (item attributes, extensions, requests) with priority-based
38+
* conflict resolution and smart filtering to prevent attribute conflicts.
3539
*/
3640
@SdkInternalApi
3741
public final class UpdateExpressionResolver {
@@ -53,19 +57,19 @@ public static Builder builder() {
5357
}
5458

5559
/**
56-
* Merges UpdateExpressions from three sources with priority: item attributes (lowest),
57-
* extension expressions (medium), request expressions (highest).
58-
*
59-
* <p><b>Steps:</b> Identify attributes used by extensions/requests to prevent REMOVE conflicts →
60+
* Merges UpdateExpressions from three sources with priority: item attributes (lowest), extension expressions (medium),
61+
* request expressions (highest).
62+
*
63+
* <p><b>Steps:</b> Identify attributes used by extensions/requests to prevent REMOVE conflicts →
6064
* create item SET/REMOVE actions → merge extensions (override item) → merge request (override all).
61-
*
65+
*
6266
* <p><b>Backward compatibility:</b> Without request expressions, behavior is identical to previous versions.
6367
* <p><b>Exceptions:</b> DynamoDbException may be thrown when the same attribute is updated by multiple sources.
6468
*
6569
* @return merged UpdateExpression, or empty if no updates needed
6670
*/
6771
public UpdateExpression resolve() {
68-
List<String> excludedFromRemoval = attributesPresentInExpressions(Arrays.asList(extensionExpression, requestExpression));
72+
Set<String> excludedFromRemoval = attributesPresentInExpressions(Arrays.asList(extensionExpression, requestExpression));
6973

7074
UpdateExpression itemSetExpression = generateItemSetExpression(itemNonKeyAttributes, tableMetadata);
7175
UpdateExpression itemRemoveExpression = generateItemRemoveExpression(itemNonKeyAttributes, excludedFromRemoval);
@@ -75,11 +79,12 @@ public UpdateExpression resolve() {
7579
return UpdateExpression.mergeExpressions(requestExpression, itemAndExtensionExpression);
7680
}
7781

78-
private static List<String> attributesPresentInExpressions(List<UpdateExpression> updateExpressions) {
82+
private static Set<String> attributesPresentInExpressions(List<UpdateExpression> updateExpressions) {
7983
return updateExpressions.stream()
84+
.filter(Objects::nonNull)
8085
.map(UpdateExpressionConverter::findAttributeNames)
8186
.flatMap(List::stream)
82-
.collect(Collectors.toList());
87+
.collect(Collectors.toSet());
8388
}
8489

8590
public static UpdateExpression generateItemSetExpression(Map<String, AttributeValue> itemMap,
@@ -92,7 +97,7 @@ public static UpdateExpression generateItemSetExpression(Map<String, AttributeVa
9297
}
9398

9499
public static UpdateExpression generateItemRemoveExpression(Map<String, AttributeValue> itemMap,
95-
List<String> nonRemoveAttributes) {
100+
Set<String> nonRemoveAttributes) {
96101
Map<String, AttributeValue> removeAttributes =
97102
filterMap(itemMap, e -> isNullAttributeValue(e.getValue()) && !nonRemoveAttributes.contains(e.getKey()));
98103

@@ -109,6 +114,7 @@ public static final class Builder {
109114
private Map<String, AttributeValue> nonKeyAttributes;
110115

111116
public Builder tableMetadata(TableMetadata tableMetadata) {
117+
requireNonNull(tableMetadata, "A TableMetadata is required when generating an Update Expression");
112118
this.tableMetadata = tableMetadata;
113119
return this;
114120
}
@@ -119,6 +125,9 @@ public Builder extensionExpression(UpdateExpression extensionExpression) {
119125
}
120126

121127
public Builder itemNonKeyAttributes(Map<String, AttributeValue> nonKeyAttributes) {
128+
if (nonKeyAttributes == null) {
129+
nonKeyAttributes = Collections.emptyMap();
130+
}
122131
this.nonKeyAttributes = nonKeyAttributes;
123132
return this;
124133
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static String ifNotExists(String key, String initValue) {
5353
/**
5454
* Creates a list of SET actions for all attributes supplied in the map.
5555
*/
56-
public static List<SetAction> setActionsFor(Map<String, AttributeValue> attributesToSet, TableMetadata tableMetadata) {
56+
static List<SetAction> setActionsFor(Map<String, AttributeValue> attributesToSet, TableMetadata tableMetadata) {
5757
return attributesToSet.entrySet()
5858
.stream()
5959
.map(entry -> setValue(entry.getKey(),
@@ -65,7 +65,7 @@ public static List<SetAction> setActionsFor(Map<String, AttributeValue> attribut
6565
/**
6666
* Creates a list of REMOVE actions for all attributes supplied in the map.
6767
*/
68-
public static List<RemoveAction> removeActionsFor(Map<String, AttributeValue> attributesToSet) {
68+
static List<RemoveAction> removeActionsFor(Map<String, AttributeValue> attributesToSet) {
6969
return attributesToSet.entrySet()
7070
.stream()
7171
.map(entry -> remove(entry.getKey()))

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,6 @@ public int hashCode() {
140140
return result;
141141
}
142142

143-
public boolean isEmpty() {
144-
return removeActions().isEmpty() && setActions().isEmpty() && deleteActions.isEmpty() && addActions.isEmpty();
145-
}
146-
147143
/**
148144
* A builder for {@link UpdateExpression}
149145
*/

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionConverterTest.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,13 @@ class UpdateExpressionConverterTest {
4747
private static final String VALUE_TOKEN = ":PRE_";
4848

4949
@Test
50-
void convert_nullUpdateExpression_returnsNullExpression() {
51-
UpdateExpression updateExpression = null;
52-
Expression expression = UpdateExpressionConverter.toExpression(updateExpression);
53-
54-
assertThat(expression).isNull();
55-
}
56-
57-
@Test
58-
void convert_emptyUpdateExpression_returnsNullExpression() {
50+
void convert_emptyExpression() {
5951
UpdateExpression updateExpression = UpdateExpression.builder().build();
6052
Expression expression = UpdateExpressionConverter.toExpression(updateExpression);
6153

62-
assertThat(expression).isNull();
54+
assertThat(expression.expression()).isEmpty();
55+
assertThat(expression.expressionNames()).isEmpty();
56+
assertThat(expression.expressionValues()).isEmpty();
6357
}
6458

6559
@Test

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionResolverTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ public void resolve_withItemAndExtensionExpression_mergesActions() {
180180
AddAction.builder()
181181
.path("extensionAttrName")
182182
.value(":extensionAttrValue")
183-
.putExpressionValue(":extensionAttrValue", AttributeValue.builder().n("1").build()) //TODOOOO
183+
.putExpressionValue(":extensionAttrValue", AttributeValue.builder().n("1").build())
184184
.build()));
185185
}
186186

@@ -317,7 +317,7 @@ public void generateItemRemoveExpression_includesOnlyNullValues() {
317317
itemMap.put("validItemAttrName", AttributeValue.builder().s("validItemAttrValue").build());
318318
itemMap.put("nullItemAttrName", AttributeValue.builder().nul(true).build());
319319

320-
UpdateExpression result = UpdateExpressionResolver.generateItemRemoveExpression(itemMap, Collections.emptyList());
320+
UpdateExpression result = UpdateExpressionResolver.generateItemRemoveExpression(itemMap, Collections.emptySet());
321321

322322
assertThat(result).isNotNull();
323323
assertThat(result.setActions()).isEmpty();
@@ -338,7 +338,7 @@ public void generateItemRemoveExpression_excludesNonRemovableAttributes() {
338338
itemMap.put("nullItemAttr2Name", AttributeValue.builder().nul(true).build());
339339

340340
UpdateExpression result = UpdateExpressionResolver.generateItemRemoveExpression(
341-
itemMap, Collections.singletonList("nullItemAttr1Name"));
341+
itemMap, Collections.singleton("nullItemAttr1Name"));
342342

343343
assertThat(result).isNotNull();
344344
assertThat(result.setActions()).isEmpty();

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/update/UpdateExpressionTest.java

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -171,44 +171,6 @@ void merge_expression_with_all_action_types() {
171171
assertThat(result.addActions()).containsExactly(addAction, extraAddAction);
172172
}
173173

174-
@Test
175-
void isEmpty_emptyExpression_returnsTrue() {
176-
UpdateExpression updateExpression = UpdateExpression.builder().build();
177-
assertThat(updateExpression.isEmpty()).isTrue();
178-
}
179-
180-
@Test
181-
void isEmpty_withRemoveAction_returnsFalse() {
182-
UpdateExpression updateExpression = UpdateExpression.builder()
183-
.addAction(removeAction)
184-
.build();
185-
assertThat(updateExpression.isEmpty()).isFalse();
186-
}
187-
188-
@Test
189-
void isEmpty_withSetAction_returnsFalse() {
190-
UpdateExpression updateExpression = UpdateExpression.builder()
191-
.addAction(setAction)
192-
.build();
193-
assertThat(updateExpression.isEmpty()).isFalse();
194-
}
195-
196-
@Test
197-
void isEmpty_withDeleteAction_returnsFalse() {
198-
UpdateExpression updateExpression = UpdateExpression.builder()
199-
.addAction(deleteAction)
200-
.build();
201-
assertThat(updateExpression.isEmpty()).isFalse();
202-
}
203-
204-
@Test
205-
void isEmpty_withAddAction_returnsFalse() {
206-
UpdateExpression updateExpression = UpdateExpression.builder()
207-
.addAction(addAction)
208-
.build();
209-
assertThat(updateExpression.isEmpty()).isFalse();
210-
}
211-
212174
private static final class UnknownUpdateAction implements UpdateAction {
213175

214176
}

0 commit comments

Comments
 (0)