Skip to content

Commit 389aee6

Browse files
committed
#7102 Fix enhanced client list projection placeholders
Signed-off-by: Arnab Nandy <arnab_nandy7@yahoo.com>
1 parent fe65375 commit 389aee6

4 files changed

Lines changed: 84 additions & 3 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon DynamoDB Enhanced Client",
4+
"description": "Fix projection expression placeholders for list-index attributes in the enhanced DynamoDB client. Resolves [#7102](https://github.com/aws/aws-sdk-java-v2/issues/7102).",
5+
"contributor": "Arnab"
6+
}

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@
9393
public class ProjectionExpression {
9494

9595
private static final String AMZN_MAPPED = "#AMZN_MAPPED_";
96-
private static final UnaryOperator<String> PROJECTION_EXPRESSION_KEY_MAPPER = k -> AMZN_MAPPED + cleanAttributeName(k);
96+
private static final UnaryOperator<String> PROJECTION_EXPRESSION_KEY_MAPPER =
97+
k -> AMZN_MAPPED + cleanAttributeName(attributeNameWithoutListDereference(k));
9798

9899
private final Optional<String> projectionExpressionAsString;
99100
private final Map<String, String> expressionAttributeNames;
@@ -132,6 +133,7 @@ private static Map<String, String> createAttributePlaceholders(List<NestedAttrib
132133
Map<String, List<String>> placeholderToAttributeNames =
133134
nestedAttributeNames.stream()
134135
.flatMap(n -> n.elements().stream())
136+
.map(ProjectionExpression::attributeNameWithoutListDereference)
135137
.distinct()
136138
.collect(Collectors.groupingBy(PROJECTION_EXPRESSION_KEY_MAPPER, Collectors.toList()));
137139

@@ -180,8 +182,45 @@ private static String convertToNameExpression(NestedAttributeName nestedAttribut
180182
Map<String, String> attributeToSanitizedMap) {
181183
return nestedAttributeName.elements()
182184
.stream()
183-
.map(attributeToSanitizedMap::get)
185+
.map(element -> attributeToSanitizedMap.get(attributeNameWithoutListDereference(element))
186+
+ listDereferenceSuffix(element))
184187
.collect(Collectors.joining("."));
185188
}
186189

190+
private static String attributeNameWithoutListDereference(String attributeName) {
191+
int firstListDereferenceIndex = firstListDereferenceIndex(attributeName);
192+
return firstListDereferenceIndex == -1 ? attributeName : attributeName.substring(0, firstListDereferenceIndex);
193+
}
194+
195+
private static String listDereferenceSuffix(String attributeName) {
196+
int firstListDereferenceIndex = firstListDereferenceIndex(attributeName);
197+
return firstListDereferenceIndex == -1 ? "" : attributeName.substring(firstListDereferenceIndex);
198+
}
199+
200+
private static int firstListDereferenceIndex(String attributeName) {
201+
int position = attributeName.length();
202+
int firstListDereferenceIndex = position;
203+
204+
while (position > 0 && attributeName.charAt(position - 1) == ']') {
205+
int openBracketIndex = attributeName.lastIndexOf('[', position - 1);
206+
if (openBracketIndex == -1 || openBracketIndex == position - 1 ||
207+
!containsOnlyDigits(attributeName, openBracketIndex + 1, position - 1)) {
208+
return -1;
209+
}
210+
firstListDereferenceIndex = openBracketIndex;
211+
position = openBracketIndex;
212+
}
213+
214+
return firstListDereferenceIndex == attributeName.length() ? -1 : firstListDereferenceIndex;
215+
}
216+
217+
private static boolean containsOnlyDigits(String value, int beginIndex, int endIndex) {
218+
for (int i = beginIndex; i < endIndex; i++) {
219+
if (!Character.isDigit(value.charAt(i))) {
220+
return false;
221+
}
222+
}
223+
return true;
224+
}
225+
187226
}

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class ProjectionExpressionTest {
2222
static {
2323
EXPECTED_ATTRIBUTE_NAMES.put("#AMZN_MAPPED_attribute", "attribute");
2424
EXPECTED_ATTRIBUTE_NAMES.put("#AMZN_MAPPED_Attribute", "Attribute");
25-
EXPECTED_ATTRIBUTE_NAMES.put("#AMZN_MAPPED_firstiteminlist[0]", "firstiteminlist[0]");
25+
EXPECTED_ATTRIBUTE_NAMES.put("#AMZN_MAPPED_firstiteminlist", "firstiteminlist");
2626
EXPECTED_ATTRIBUTE_NAMES.put("#AMZN_MAPPED_March_2021", "March-2021");
2727
EXPECTED_ATTRIBUTE_NAMES.put("#AMZN_MAPPED_Why_Make_This_An_Attribute_Name", "Why.Make-This*An:Attribute:Name");
2828
}
@@ -104,6 +104,23 @@ public void nonUniquePlaceholders_AreDisambiguated() {
104104
assertProjectionExpression(attributeNames, expectedAttributeNames, expectedProjectionExpression);
105105
}
106106

107+
@Test
108+
public void listDereferenceAttributes_AreMappedToAttributeName() {
109+
Map<String, String> expectedAttributeNames = new HashMap<>();
110+
expectedAttributeNames.put("#AMZN_MAPPED_tags", "tags");
111+
expectedAttributeNames.put("#AMZN_MAPPED_metadata", "metadata");
112+
expectedAttributeNames.put("#AMZN_MAPPED_values", "values");
113+
114+
List<NestedAttributeName> attributeNames = Arrays.asList(
115+
NestedAttributeName.create("tags[0]"),
116+
NestedAttributeName.create("metadata", "values[1][2]")
117+
);
118+
119+
String expectedProjectionExpression = "#AMZN_MAPPED_tags[0],#AMZN_MAPPED_metadata.#AMZN_MAPPED_values[1][2]";
120+
121+
assertProjectionExpression(attributeNames, expectedAttributeNames, expectedProjectionExpression);
122+
}
123+
107124
private void assertProjectionExpression(List<NestedAttributeName> attributeNames,
108125
Map<String, String> expectedAttributeNames,
109126
String expectedProjectionExpression) {

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/ScanOperationTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,25 @@ public void generateRequest_projectionExpression() {
229229
assertThat(request, is(expectedRequest));
230230
}
231231

232+
@Test
233+
public void generateRequest_projectionExpressionWithListDereference() {
234+
ScanOperation<FakeItem> operation = ScanOperation.create(
235+
ScanEnhancedRequest.builder()
236+
.addAttributeToProject("tags[0]")
237+
.build()
238+
);
239+
ScanRequest request = operation.generateRequest(FakeItem.getTableSchema(),
240+
PRIMARY_CONTEXT,
241+
null);
242+
243+
ScanRequest expectedRequest = ScanRequest.builder()
244+
.tableName(TABLE_NAME)
245+
.projectionExpression("#AMZN_MAPPED_tags[0]")
246+
.expressionAttributeNames(singletonMap("#AMZN_MAPPED_tags", "tags"))
247+
.build();
248+
assertThat(request, is(expectedRequest));
249+
}
250+
232251
@Test
233252
public void generateRequest_hashKeyOnly_exclusiveStartKey() {
234253
FakeItem exclusiveStartKey = createUniqueFakeItem();

0 commit comments

Comments
 (0)