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