Skip to content

Commit 248dbb4

Browse files
committed
DynamoDb enhanced client: support UpdateExpressions in single-request update
1 parent 7220c9e commit 248dbb4

File tree

2 files changed

+387
-16
lines changed

2 files changed

+387
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb.internal.update;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.Mockito.mock;
20+
21+
import java.util.Arrays;
22+
import java.util.Collections;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import org.junit.Test;
26+
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
27+
import software.amazon.awssdk.enhanced.dynamodb.update.AddAction;
28+
import software.amazon.awssdk.enhanced.dynamodb.update.RemoveAction;
29+
import software.amazon.awssdk.enhanced.dynamodb.update.SetAction;
30+
import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression;
31+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
32+
33+
public class UpdateExpressionResolverTest {
34+
35+
private final TableMetadata mockTableMetadata = mock(TableMetadata.class);
36+
37+
@Test
38+
public void resolve_emptyInputs_returnsEmptyUpdateExpression() {
39+
UpdateExpressionResolver resolver = UpdateExpressionResolver.builder()
40+
.tableMetadata(mockTableMetadata)
41+
.itemNonKeyAttributes(Collections.emptyMap())
42+
.build();
43+
44+
UpdateExpression result = resolver.resolve();
45+
46+
assertThat(result).isEqualTo(UpdateExpression.builder().build());
47+
}
48+
49+
@Test
50+
public void resolve_nonNullAttributes_generatesSetActions() {
51+
Map<String, AttributeValue> itemMap = new HashMap<>();
52+
itemMap.put("itemAttr1Name", AttributeValue.builder().s("itemAttr1Value").build());
53+
itemMap.put("itemAttr2Name", AttributeValue.builder().n("itemAttr2Value").build());
54+
55+
UpdateExpressionResolver resolver = UpdateExpressionResolver.builder()
56+
.tableMetadata(mockTableMetadata)
57+
.itemNonKeyAttributes(itemMap)
58+
.build();
59+
60+
UpdateExpression result = resolver.resolve();
61+
62+
assertThat(result).isNotNull();
63+
assertThat(result.removeActions()).isEmpty();
64+
assertThat(result.addActions()).isEmpty();
65+
assertThat(result.deleteActions()).isEmpty();
66+
67+
assertThat(result.setActions()).hasSize(2).containsExactlyInAnyOrder(
68+
SetAction.builder()
69+
.path("#AMZN_MAPPED_itemAttr1Name")
70+
.value(":AMZN_MAPPED_itemAttr1Name")
71+
.putExpressionName("#AMZN_MAPPED_itemAttr1Name", "itemAttr1Name")
72+
.putExpressionValue(":AMZN_MAPPED_itemAttr1Name", AttributeValue.builder().s("itemAttr1Value").build())
73+
.build(),
74+
75+
SetAction.builder()
76+
.path("#AMZN_MAPPED_itemAttr2Name")
77+
.value(":AMZN_MAPPED_itemAttr2Name")
78+
.putExpressionName("#AMZN_MAPPED_itemAttr2Name", "itemAttr2Name")
79+
.putExpressionValue(":AMZN_MAPPED_itemAttr2Name", AttributeValue.builder().n("itemAttr2Value").build())
80+
.build());
81+
}
82+
83+
@Test
84+
public void resolve_nullAttributes_generatesRemoveActions() {
85+
Map<String, AttributeValue> itemMap = new HashMap<>();
86+
itemMap.put("itemAttr1Name", AttributeValue.builder().nul(true).build());
87+
itemMap.put("itemAttr2Name", AttributeValue.builder().nul(true).build());
88+
89+
UpdateExpressionResolver resolver = UpdateExpressionResolver.builder()
90+
.tableMetadata(mockTableMetadata)
91+
.itemNonKeyAttributes(itemMap)
92+
.build();
93+
94+
UpdateExpression result = resolver.resolve();
95+
96+
assertThat(result).isNotNull();
97+
assertThat(result.setActions()).isEmpty();
98+
assertThat(result.addActions()).isEmpty();
99+
assertThat(result.deleteActions()).isEmpty();
100+
101+
assertThat(result.removeActions()).hasSize(2).containsExactlyInAnyOrder(
102+
RemoveAction.builder()
103+
.path("#AMZN_MAPPED_itemAttr1Name")
104+
.putExpressionName("#AMZN_MAPPED_itemAttr1Name", "itemAttr1Name")
105+
.build(),
106+
107+
RemoveAction.builder()
108+
.path("#AMZN_MAPPED_itemAttr2Name")
109+
.putExpressionName("#AMZN_MAPPED_itemAttr2Name", "itemAttr2Name")
110+
.build());
111+
}
112+
113+
@Test
114+
public void resolve_mixedAttributes_generatesBothActions() {
115+
Map<String, AttributeValue> itemMap = new HashMap<>();
116+
itemMap.put("setAttrName", AttributeValue.builder().s("setAttrValue").build());
117+
itemMap.put("removeAttrName", AttributeValue.builder().nul(true).build());
118+
119+
UpdateExpressionResolver resolver = UpdateExpressionResolver.builder()
120+
.tableMetadata(mockTableMetadata)
121+
.itemNonKeyAttributes(itemMap)
122+
.build();
123+
124+
UpdateExpression result = resolver.resolve();
125+
126+
assertThat(result).isNotNull();
127+
assertThat(result.addActions()).isEmpty();
128+
assertThat(result.deleteActions()).isEmpty();
129+
130+
assertThat(result.setActions()).isEqualTo(Collections.singletonList(
131+
SetAction.builder()
132+
.path("#AMZN_MAPPED_setAttrName")
133+
.value(":AMZN_MAPPED_setAttrName")
134+
.putExpressionName("#AMZN_MAPPED_setAttrName", "setAttrName")
135+
.putExpressionValue(":AMZN_MAPPED_setAttrName", AttributeValue.builder().s("setAttrValue").build())
136+
.build()));
137+
138+
assertThat(result.removeActions()).isEqualTo(Collections.singletonList(
139+
RemoveAction.builder()
140+
.path("#AMZN_MAPPED_removeAttrName")
141+
.putExpressionName("#AMZN_MAPPED_removeAttrName", "removeAttrName")
142+
.build()));
143+
}
144+
145+
@Test
146+
public void resolve_withItemAndExtensionExpression_mergesActions() {
147+
Map<String, AttributeValue> itemMap = new HashMap<>();
148+
itemMap.put("itemAttrName", AttributeValue.builder().s("itemAttrValue").build());
149+
150+
UpdateExpression extensionExpression =
151+
UpdateExpression.builder()
152+
.addAction(AddAction.builder()
153+
.path("extensionAttrName")
154+
.value(":extensionAttrValue")
155+
.putExpressionValue(":extensionAttrValue",
156+
AttributeValue.builder().n("1").build())
157+
.build())
158+
.build();
159+
160+
UpdateExpressionResolver resolver = UpdateExpressionResolver.builder()
161+
.tableMetadata(mockTableMetadata)
162+
.itemNonKeyAttributes(itemMap)
163+
.transformationExpression(extensionExpression)
164+
.build();
165+
166+
UpdateExpression result = resolver.resolve();
167+
168+
assertThat(result).isNotNull();
169+
assertThat(result.removeActions()).isEmpty();
170+
assertThat(result.deleteActions()).isEmpty();
171+
172+
assertThat(result.setActions()).isEqualTo(Collections.singletonList(
173+
SetAction.builder()
174+
.path("#AMZN_MAPPED_itemAttrName")
175+
.value(":AMZN_MAPPED_itemAttrName")
176+
.putExpressionName("#AMZN_MAPPED_itemAttrName", "itemAttrName")
177+
.putExpressionValue(":AMZN_MAPPED_itemAttrName", AttributeValue.builder().s("itemAttrValue").build())
178+
.build()));
179+
180+
assertThat(result.addActions()).isEqualTo(Collections.singletonList(
181+
AddAction.builder()
182+
.path("extensionAttrName")
183+
.value(":extensionAttrValue")
184+
.putExpressionValue(":extensionAttrValue", AttributeValue.builder().n("1").build()) //TODOOOO
185+
.build()));
186+
}
187+
188+
@Test
189+
public void resolve_withAllExpressionTypes_mergesInCorrectOrder() {
190+
Map<String, AttributeValue> itemMap = new HashMap<>();
191+
itemMap.put("itemAttrName", AttributeValue.builder().s("itemAttrValue").build());
192+
193+
UpdateExpression extensionExpression =
194+
UpdateExpression.builder()
195+
.addAction(AddAction.builder()
196+
.path("extensionAttrName")
197+
.value(":extensionAttrName")
198+
.putExpressionValue(":extensionAttrName", AttributeValue.builder().s(
199+
"extensionAttrValue").build())
200+
.build())
201+
.build();
202+
203+
UpdateExpression requestExpression =
204+
UpdateExpression.builder()
205+
.addAction(SetAction.builder()
206+
.path("requestAttrName")
207+
.value(":requestAttrName")
208+
.putExpressionValue(":requestAttrName", AttributeValue.builder().s(
209+
"requestAttrValue").build())
210+
.build())
211+
.build();
212+
213+
UpdateExpressionResolver resolver = UpdateExpressionResolver.builder()
214+
.tableMetadata(mockTableMetadata)
215+
.itemNonKeyAttributes(itemMap)
216+
.transformationExpression(extensionExpression)
217+
.requestExpression(requestExpression)
218+
.build();
219+
220+
UpdateExpression result = resolver.resolve();
221+
222+
assertThat(result).isNotNull();
223+
assertThat(result.removeActions()).isEmpty();
224+
assertThat(result.deleteActions()).isEmpty();
225+
226+
assertThat(result.setActions()).hasSize(2).containsExactlyInAnyOrder(
227+
SetAction.builder()
228+
.path("#AMZN_MAPPED_itemAttrName")
229+
.value(":AMZN_MAPPED_itemAttrName")
230+
.putExpressionName("#AMZN_MAPPED_itemAttrName", "itemAttrName")
231+
.putExpressionValue(":AMZN_MAPPED_itemAttrName", AttributeValue.builder().s("itemAttrValue").build())
232+
.build(),
233+
SetAction.builder()
234+
.path("requestAttrName")
235+
.value(":requestAttrName")
236+
.putExpressionValue(":requestAttrName", AttributeValue.builder().s("requestAttrValue").build())
237+
.build());
238+
239+
assertThat(result.addActions()).isEqualTo(Collections.singletonList(
240+
AddAction.builder()
241+
.path("extensionAttrName")
242+
.value(":extensionAttrName")
243+
.putExpressionValue(":extensionAttrName", AttributeValue.builder().s("extensionAttrValue").build())
244+
.build()));
245+
}
246+
247+
@Test
248+
public void resolve_attributeUsedInOtherExpression_filteredOutFromRemoveActions() {
249+
Map<String, AttributeValue> itemMap = new HashMap<>();
250+
itemMap.put("itemAttr1Name", AttributeValue.builder().nul(true).build());
251+
itemMap.put("itemAttr2Name", AttributeValue.builder().nul(true).build());
252+
253+
UpdateExpression requestExpression =
254+
UpdateExpression.builder()
255+
.addAction(SetAction.builder()
256+
.path("itemAttr1Name")
257+
.value(":itemAttr1Value")
258+
.putExpressionName("#itemAttr1Name", "itemAttr1Name")
259+
.putExpressionValue(":itemAttr1Value", AttributeValue.builder().s(
260+
"itemAttr1Value_new").build())
261+
.build())
262+
.build();
263+
264+
UpdateExpressionResolver resolver = UpdateExpressionResolver.builder()
265+
.tableMetadata(mockTableMetadata)
266+
.itemNonKeyAttributes(itemMap)
267+
.requestExpression(requestExpression)
268+
.build();
269+
270+
UpdateExpression result = resolver.resolve();
271+
272+
assertThat(result).isNotNull();
273+
assertThat(result.addActions()).isEmpty();
274+
assertThat(result.deleteActions()).isEmpty();
275+
276+
assertThat(result.setActions()).isEqualTo(Collections.singletonList(
277+
SetAction.builder()
278+
.path("itemAttr1Name")
279+
.value(":itemAttr1Value")
280+
.putExpressionName("#itemAttr1Name", "itemAttr1Name")
281+
.putExpressionValue(":itemAttr1Value", AttributeValue.builder().s("itemAttr1Value_new").build())
282+
.build()));
283+
284+
// only itemAttr2Name, itemAttr1Name filtered out (because was present in a set expression)
285+
assertThat(result.removeActions()).isEqualTo(Collections.singletonList(
286+
RemoveAction.builder()
287+
.path("#AMZN_MAPPED_itemAttr2Name")
288+
.putExpressionName("#AMZN_MAPPED_itemAttr2Name", "itemAttr2Name")
289+
.build()));
290+
}
291+
292+
@Test
293+
public void generateItemSetExpression_andFiltersNullValues() {
294+
Map<String, AttributeValue> itemMap = new HashMap<>();
295+
itemMap.put("validItemAttrName", AttributeValue.builder().s("validItemAttrValue").build());
296+
itemMap.put("nullItemAttrName", AttributeValue.builder().nul(true).build());
297+
298+
UpdateExpression result = UpdateExpressionResolver.generateItemSetExpression(itemMap, mockTableMetadata);
299+
300+
assertThat(result).isNotNull();
301+
assertThat(result.removeActions()).isEmpty();
302+
assertThat(result.addActions()).isEmpty();
303+
assertThat(result.deleteActions()).isEmpty();
304+
305+
assertThat(result.setActions()).isEqualTo(Collections.singletonList(
306+
SetAction.builder()
307+
.path("#AMZN_MAPPED_validItemAttrName")
308+
.value(":AMZN_MAPPED_validItemAttrName")
309+
.putExpressionName("#AMZN_MAPPED_validItemAttrName", "validItemAttrName")
310+
.putExpressionValue(":AMZN_MAPPED_validItemAttrName",
311+
AttributeValue.builder().s("validItemAttrValue").build())
312+
.build()));
313+
}
314+
315+
@Test
316+
public void generateItemRemoveExpression_includesOnlyNullValues() {
317+
Map<String, AttributeValue> itemMap = new HashMap<>();
318+
itemMap.put("validItemAttrName", AttributeValue.builder().s("validItemAttrValue").build());
319+
itemMap.put("nullItemAttrName", AttributeValue.builder().nul(true).build());
320+
321+
UpdateExpression result = UpdateExpressionResolver.generateItemRemoveExpression(itemMap, Collections.emptyList());
322+
323+
assertThat(result).isNotNull();
324+
assertThat(result.setActions()).isEmpty();
325+
assertThat(result.addActions()).isEmpty();
326+
assertThat(result.deleteActions()).isEmpty();
327+
328+
assertThat(result.removeActions()).isEqualTo(Collections.singletonList(
329+
RemoveAction.builder()
330+
.path("#AMZN_MAPPED_nullItemAttrName")
331+
.putExpressionName("#AMZN_MAPPED_nullItemAttrName", "nullItemAttrName")
332+
.build()));
333+
}
334+
335+
@Test
336+
public void generateItemRemoveExpression_excludesNonRemovableAttributes() {
337+
Map<String, AttributeValue> itemMap = new HashMap<>();
338+
itemMap.put("nullItemAttr1Name", AttributeValue.builder().nul(true).build());
339+
itemMap.put("nullItemAttr2Name", AttributeValue.builder().nul(true).build());
340+
341+
UpdateExpression result = UpdateExpressionResolver.generateItemRemoveExpression(
342+
itemMap, Collections.singletonList("nullItemAttr1Name"));
343+
344+
assertThat(result).isNotNull();
345+
assertThat(result.setActions()).isEmpty();
346+
assertThat(result.addActions()).isEmpty();
347+
assertThat(result.deleteActions()).isEmpty();
348+
349+
assertThat(result.removeActions()).isEqualTo(Collections.singletonList(
350+
RemoveAction.builder()
351+
.path("#AMZN_MAPPED_nullItemAttr2Name")
352+
.putExpressionName("#AMZN_MAPPED_nullItemAttr2Name", "nullItemAttr2Name")
353+
.build()));
354+
}
355+
356+
@Test
357+
public void builder_allFields_buildsSuccessfully() {
358+
Map<String, AttributeValue> itemMap = new HashMap<>();
359+
UpdateExpression extensionExpr = UpdateExpression.builder().build();
360+
UpdateExpression requestExpr = UpdateExpression.builder().build();
361+
362+
UpdateExpressionResolver resolver = UpdateExpressionResolver.builder()
363+
.tableMetadata(mockTableMetadata)
364+
.itemNonKeyAttributes(itemMap)
365+
.transformationExpression(extensionExpr)
366+
.requestExpression(requestExpr)
367+
.build();
368+
369+
assertThat(resolver).isNotNull();
370+
}
371+
}

0 commit comments

Comments
 (0)