Skip to content

Commit 5b67967

Browse files
authored
Fixed bug in VersionedRecord that did not allow special characters in version key name, github issues [#2507](#2507), [#2950](#2950), [#3279](#3279) (#3282)
1 parent a4b87c1 commit 5b67967

File tree

11 files changed

+345
-64
lines changed

11 files changed

+345
-64
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "Amazon DynamoDB Enhanced",
3+
"contributor": "",
4+
"type": "bugfix",
5+
"description": "Adding usage of ExpressionNames to VersionedRecord, thereby allowing version attributes with reserved words and special characters"
6+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AtomicCounterExtension.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515

1616
package software.amazon.awssdk.enhanced.dynamodb.extensions;
1717

18+
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef;
19+
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef;
1820
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.ifNotExists;
19-
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.keyRef;
20-
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.valueRef;
2121

2222
import java.util.Collections;
2323
import java.util.HashMap;

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtension.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.enhanced.dynamodb.extensions;
1717

1818
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
19+
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef;
1920

2021
import java.util.Collections;
2122
import java.util.HashMap;
@@ -56,7 +57,7 @@
5657
@SdkPublicApi
5758
@ThreadSafe
5859
public final class VersionedRecordExtension implements DynamoDbEnhancedClientExtension {
59-
private static final Function<String, String> EXPRESSION_KEY_MAPPER = key -> ":old_" + key + "_value";
60+
private static final Function<String, String> VERSIONED_RECORD_EXPRESSION_VALUE_KEY_MAPPER = key -> ":old_" + key + "_value";
6061
private static final String CUSTOM_METADATA_KEY = "VersionedRecordExtension:VersionAttribute";
6162
private static final VersionAttribute VERSION_ATTRIBUTE = new VersionAttribute();
6263

@@ -101,6 +102,8 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
101102
}
102103

103104
Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());
105+
106+
String attributeKeyRef = keyRef(versionAttributeKey.get());
104107
AttributeValue newVersionValue;
105108
Expression condition;
106109
Optional<AttributeValue> existingVersionValue =
@@ -110,7 +113,8 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
110113
// First version of the record
111114
newVersionValue = AttributeValue.builder().n("1").build();
112115
condition = Expression.builder()
113-
.expression(String.format("attribute_not_exists(%s)", versionAttributeKey.get()))
116+
.expression(String.format("attribute_not_exists(%s)", attributeKeyRef))
117+
.expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeKey.get()))
114118
.build();
115119
} else {
116120
// Existing record, increment version
@@ -120,11 +124,11 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
120124
}
121125

122126
int existingVersion = Integer.parseInt(existingVersionValue.get().n());
123-
String existingVersionValueKey = EXPRESSION_KEY_MAPPER.apply(versionAttributeKey.get());
127+
String existingVersionValueKey = VERSIONED_RECORD_EXPRESSION_VALUE_KEY_MAPPER.apply(versionAttributeKey.get());
124128
newVersionValue = AttributeValue.builder().n(Integer.toString(existingVersion + 1)).build();
125129
condition = Expression.builder()
126-
.expression(String.format("%s = %s", versionAttributeKey.get(),
127-
existingVersionValueKey))
130+
.expression(String.format("%s = %s", attributeKeyRef, existingVersionValueKey))
131+
.expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeKey.get()))
128132
.expressionValues(Collections.singletonMap(existingVersionValueKey,
129133
existingVersionValue.get()))
130134
.build();

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ public static String cleanAttributeName(String key) {
6767
return somethingChanged ? new String(chars) : key;
6868
}
6969

70+
/**
71+
* Creates a key token to be used with an ExpressionNames map.
72+
*/
73+
public static String keyRef(String key) {
74+
return "#AMZN_MAPPED_" + cleanAttributeName(key);
75+
}
76+
77+
/**
78+
* Creates a value token to be used with an ExpressionValues map.
79+
*/
80+
public static String valueRef(String value) {
81+
return ":AMZN_MAPPED_" + cleanAttributeName(value);
82+
}
83+
7084
public static <T> T readAndTransformSingleItem(Map<String, AttributeValue> itemMap,
7185
TableSchema<T> tableSchema,
7286
OperationContext operationContext,

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,20 @@
1616
package software.amazon.awssdk.enhanced.dynamodb.internal.conditional;
1717

1818
import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue;
19-
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.cleanAttributeName;
2019

2120
import java.util.Collections;
2221
import java.util.HashMap;
2322
import java.util.Map;
24-
import java.util.function.UnaryOperator;
2523
import software.amazon.awssdk.annotations.SdkInternalApi;
2624
import software.amazon.awssdk.enhanced.dynamodb.Expression;
2725
import software.amazon.awssdk.enhanced.dynamodb.Key;
2826
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
27+
import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils;
2928
import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
3029
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
3130

3231
@SdkInternalApi
3332
public class BeginsWithConditional implements QueryConditional {
34-
private static final UnaryOperator<String> EXPRESSION_KEY_MAPPER =
35-
k -> "#AMZN_MAPPED_" + cleanAttributeName(k);
36-
private static final UnaryOperator<String> EXPRESSION_VALUE_KEY_MAPPER =
37-
k -> ":AMZN_MAPPED_" + cleanAttributeName(k);
3833

3934
private final Key key;
4035

@@ -56,10 +51,10 @@ public Expression expression(TableSchema<?> tableSchema, String indexName) {
5651
+ "a numeric sort key.");
5752
}
5853

59-
String partitionKeyToken = EXPRESSION_KEY_MAPPER.apply(queryConditionalKeyValues.partitionKey());
60-
String partitionValueToken = EXPRESSION_VALUE_KEY_MAPPER.apply(queryConditionalKeyValues.partitionKey());
61-
String sortKeyToken = EXPRESSION_KEY_MAPPER.apply(queryConditionalKeyValues.sortKey());
62-
String sortValueToken = EXPRESSION_VALUE_KEY_MAPPER.apply(queryConditionalKeyValues.sortKey());
54+
String partitionKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues.partitionKey());
55+
String partitionValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues.partitionKey());
56+
String sortKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues.sortKey());
57+
String sortValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues.sortKey());
6358

6459
String queryExpression = String.format("%s = %s AND begins_with ( %s, %s )",
6560
partitionKeyToken,

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@
2626
import software.amazon.awssdk.enhanced.dynamodb.Expression;
2727
import software.amazon.awssdk.enhanced.dynamodb.Key;
2828
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
29+
import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils;
2930
import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
3031
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
3132

3233
@SdkInternalApi
3334
public class BetweenConditional implements QueryConditional {
34-
private static final UnaryOperator<String> EXPRESSION_KEY_MAPPER =
35-
k -> "#AMZN_MAPPED_" + cleanAttributeName(k);
36-
private static final UnaryOperator<String> EXPRESSION_VALUE_KEY_MAPPER =
37-
k -> ":AMZN_MAPPED_" + cleanAttributeName(k);
35+
3836
private static final UnaryOperator<String> EXPRESSION_OTHER_VALUE_KEY_MAPPER =
3937
k -> ":AMZN_MAPPED_" + cleanAttributeName(k) + "2";
4038

@@ -57,10 +55,10 @@ public Expression expression(TableSchema<?> tableSchema, String indexName) {
5755
+ "of the items has a null sort key.");
5856
}
5957

60-
String partitionKeyToken = EXPRESSION_KEY_MAPPER.apply(queryConditionalKeyValues1.partitionKey());
61-
String partitionValueToken = EXPRESSION_VALUE_KEY_MAPPER.apply(queryConditionalKeyValues1.partitionKey());
62-
String sortKeyToken = EXPRESSION_KEY_MAPPER.apply(queryConditionalKeyValues1.sortKey());
63-
String sortKeyValueToken1 = EXPRESSION_VALUE_KEY_MAPPER.apply(queryConditionalKeyValues1.sortKey());
58+
String partitionKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues1.partitionKey());
59+
String partitionValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues1.partitionKey());
60+
String sortKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues1.sortKey());
61+
String sortKeyValueToken1 = EnhancedClientUtils.valueRef(queryConditionalKeyValues1.sortKey());
6462
String sortKeyValueToken2 = EXPRESSION_OTHER_VALUE_KEY_MAPPER.apply(queryConditionalKeyValues2.sortKey());
6563

6664
String queryExpression = String.format("%s = %s AND %s BETWEEN %s AND %s",

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

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,22 @@
1616
package software.amazon.awssdk.enhanced.dynamodb.internal.conditional;
1717

1818
import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue;
19-
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.cleanAttributeName;
2019
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
2120

2221
import java.util.Collections;
2322
import java.util.HashMap;
2423
import java.util.Map;
2524
import java.util.Optional;
26-
import java.util.function.UnaryOperator;
2725
import software.amazon.awssdk.annotations.SdkInternalApi;
2826
import software.amazon.awssdk.enhanced.dynamodb.Expression;
2927
import software.amazon.awssdk.enhanced.dynamodb.Key;
3028
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
29+
import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils;
3130
import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
3231
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
3332

3433
@SdkInternalApi
3534
public class EqualToConditional implements QueryConditional {
36-
private static final UnaryOperator<String> EXPRESSION_KEY_MAPPER =
37-
k -> "#AMZN_MAPPED_" + cleanAttributeName(k);
38-
private static final UnaryOperator<String> EXPRESSION_VALUE_KEY_MAPPER =
39-
k -> ":AMZN_MAPPED_" + cleanAttributeName(k);
4035

4136
private final Key key;
4237

@@ -77,8 +72,8 @@ public Expression expression(TableSchema<?> tableSchema, String indexName) {
7772
private Expression partitionOnlyExpression(String partitionKey,
7873
AttributeValue partitionValue) {
7974

80-
String partitionKeyToken = EXPRESSION_KEY_MAPPER.apply(partitionKey);
81-
String partitionKeyValueToken = EXPRESSION_VALUE_KEY_MAPPER.apply(partitionKey);
75+
String partitionKeyToken = EnhancedClientUtils.keyRef(partitionKey);
76+
String partitionKeyValueToken = EnhancedClientUtils.valueRef(partitionKey);
8277
String queryExpression = String.format("%s = %s", partitionKeyToken, partitionKeyValueToken);
8378

8479
return Expression.builder()
@@ -99,10 +94,10 @@ private Expression partitionAndSortExpression(String partitionKey,
9994
return partitionOnlyExpression(partitionKey, partitionValue);
10095
}
10196

102-
String partitionKeyToken = EXPRESSION_KEY_MAPPER.apply(partitionKey);
103-
String partitionKeyValueToken = EXPRESSION_VALUE_KEY_MAPPER.apply(partitionKey);
104-
String sortKeyToken = EXPRESSION_KEY_MAPPER.apply(sortKey);
105-
String sortKeyValueToken = EXPRESSION_VALUE_KEY_MAPPER.apply(sortKey);
97+
String partitionKeyToken = EnhancedClientUtils.keyRef(partitionKey);
98+
String partitionKeyValueToken = EnhancedClientUtils.valueRef(partitionKey);
99+
String sortKeyToken = EnhancedClientUtils.keyRef(sortKey);
100+
String sortKeyValueToken = EnhancedClientUtils.valueRef(sortKey);
106101

107102
String queryExpression = String.format("%s = %s AND %s = %s",
108103
partitionKeyToken,

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616
package software.amazon.awssdk.enhanced.dynamodb.internal.conditional;
1717

1818
import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue;
19-
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.cleanAttributeName;
2019

2120
import java.util.HashMap;
2221
import java.util.Map;
23-
import java.util.function.UnaryOperator;
2422
import software.amazon.awssdk.annotations.SdkInternalApi;
2523
import software.amazon.awssdk.enhanced.dynamodb.Expression;
2624
import software.amazon.awssdk.enhanced.dynamodb.Key;
2725
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
26+
import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils;
2827
import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
2928
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
3029

@@ -37,10 +36,6 @@
3736
*/
3837
@SdkInternalApi
3938
public class SingleKeyItemConditional implements QueryConditional {
40-
private static final UnaryOperator<String> EXPRESSION_KEY_MAPPER =
41-
k -> "#AMZN_MAPPED_" + cleanAttributeName(k);
42-
private static final UnaryOperator<String> EXPRESSION_VALUE_KEY_MAPPER =
43-
k -> ":AMZN_MAPPED_" + cleanAttributeName(k);
4439

4540
private final Key key;
4641
private final String operator;
@@ -59,10 +54,10 @@ public Expression expression(TableSchema<?> tableSchema, String indexName) {
5954
+ "null sort key.");
6055
}
6156

62-
String partitionKeyToken = EXPRESSION_KEY_MAPPER.apply(queryConditionalKeyValues.partitionKey());
63-
String partitionValueToken = EXPRESSION_VALUE_KEY_MAPPER.apply(queryConditionalKeyValues.partitionKey());
64-
String sortKeyToken = EXPRESSION_KEY_MAPPER.apply(queryConditionalKeyValues.sortKey());
65-
String sortValueToken = EXPRESSION_VALUE_KEY_MAPPER.apply(queryConditionalKeyValues.sortKey());
57+
String partitionKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues.partitionKey());
58+
String partitionValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues.partitionKey());
59+
String sortKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues.sortKey());
60+
String sortValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues.sortKey());
6661

6762
String queryExpression = String.format("%s = %s AND %s %s %s",
6863
partitionKeyToken,

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

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package software.amazon.awssdk.enhanced.dynamodb.internal.update;
1717

1818
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
19+
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef;
20+
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef;
1921
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;
2022

2123
import java.util.Arrays;
@@ -40,20 +42,6 @@ public final class UpdateExpressionUtils {
4042
private UpdateExpressionUtils() {
4143
}
4244

43-
/**
44-
* Creates a key token to be used with an ExpressionNames map.
45-
*/
46-
public static String keyRef(String key) {
47-
return "#AMZN_MAPPED_" + EnhancedClientUtils.cleanAttributeName(key);
48-
}
49-
50-
/**
51-
* Creates a value token to be used with an ExpressionValues map.
52-
*/
53-
public static String valueRef(String value) {
54-
return ":AMZN_MAPPED_" + EnhancedClientUtils.cleanAttributeName(value);
55-
}
56-
5745
/**
5846
* A function to specify an initial value if the attribute represented by 'key' does not exist.
5947
*/
@@ -150,7 +138,7 @@ private static Function<String, String> behaviorBasedValue(UpdateBehavior update
150138
*/
151139
private static Map<String, String> expressionNamesFor(String... attributeNames) {
152140
return Arrays.stream(attributeNames)
153-
.collect(Collectors.toMap(UpdateExpressionUtils::keyRef, Function.identity()));
141+
.collect(Collectors.toMap(EnhancedClientUtils::keyRef, Function.identity()));
154142
}
155143

156144
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ public void beforeWrite_initialVersion_expressionIsCorrect() {
6868
.operationContext(PRIMARY_CONTEXT).build());
6969

7070
assertThat(result.additionalConditionalExpression(),
71-
is(Expression.builder().expression("attribute_not_exists(version)").build()));
71+
is(Expression.builder()
72+
.expression("attribute_not_exists(#AMZN_MAPPED_version)")
73+
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
74+
.build()));
7275
}
7376

7477
@Test
@@ -123,7 +126,8 @@ public void beforeWrite_existingVersion_expressionIsCorrect() {
123126

124127
assertThat(result.additionalConditionalExpression(),
125128
is(Expression.builder()
126-
.expression("version = :old_version_value")
129+
.expression("#AMZN_MAPPED_version = :old_version_value")
130+
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
127131
.expressionValues(singletonMap(":old_version_value",
128132
AttributeValue.builder().n("13").build()))
129133
.build()));

0 commit comments

Comments
 (0)