Skip to content

Commit 54a79e8

Browse files
Andy Kieslerakiesler
authored andcommitted
Add support for starting DynamoDB Record Version with explicit value
The prior behavior required that a version be initialized with a null value, this required mapper clients to use Integer instead of the int primitive. This change allows clients to explicitly initialize the version to a value which makes it simpler for clients to use primitive values and potentially avoid null pointer exceptions and checks. The default starting value of 0 and increment value of 1 are intended to provide sane defaults that are identical to the existing behavior while enabling clients to have more fine-graned control over how the versioning is managed for their specific use-cases. The current implementation configures the values at the extension level only but the implementation can be expanded to gather the value from the model annotation to customize the values on a per table basis.
1 parent 19ce372 commit 54a79e8

File tree

4 files changed

+92
-10
lines changed

4 files changed

+92
-10
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "Amazon DyanmoDB Enhanced Client",
3+
"contributor": "kiesler",
4+
"type": "feature",
5+
"description": "DynamoDB Enhanced Client Versioned Record can start at 0"
6+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,15 @@ public int hashCode() {
311311
return result;
312312
}
313313

314+
@Override
315+
public String toString() {
316+
return "Expression{" +
317+
"expression='" + expression + '\'' +
318+
", expressionValues=" + expressionValues +
319+
", expressionNames=" + expressionNames +
320+
'}';
321+
}
322+
314323
/**
315324
* A builder for {@link Expression}
316325
*/

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

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,20 @@ public final class VersionedRecordExtension implements DynamoDbEnhancedClientExt
6161
private static final Function<String, String> VERSIONED_RECORD_EXPRESSION_VALUE_KEY_MAPPER = key -> ":old_" + key + "_value";
6262
private static final String CUSTOM_METADATA_KEY = "VersionedRecordExtension:VersionAttribute";
6363
private static final VersionAttribute VERSION_ATTRIBUTE = new VersionAttribute();
64-
65-
private VersionedRecordExtension() {
64+
private static final AttributeValue DEFAULT_VALUE = AttributeValue.fromNul(Boolean.TRUE);
65+
66+
private final int startingValue;
67+
private final int increment;
68+
69+
/**
70+
* Creates a new {@link VersionedRecordExtension} using the supplied starting and incrementing value.
71+
*
72+
* @param startingValue the value used to compare if a record is the initial version of a record.
73+
* @param increment the amount to increment the version by with each subsequent update.
74+
*/
75+
private VersionedRecordExtension(int startingValue, int increment) {
76+
this.startingValue = startingValue;
77+
this.increment = increment;
6678
}
6779

6880
public static Builder builder() {
@@ -119,23 +131,24 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
119131

120132
private Pair<AttributeValue, Expression> getRecordUpdates(String versionAttributeKey,
121133
Map<String, AttributeValue> itemToTransform) {
122-
Optional<AttributeValue> existingVersionValue =
123-
Optional.ofNullable(itemToTransform.get(versionAttributeKey));
134+
// Default to NUL if not present to reduce additional checks further along
135+
AttributeValue existingVersionValue = itemToTransform.getOrDefault(versionAttributeKey, DEFAULT_VALUE);
124136

125137
if (isInitialVersion(existingVersionValue)) {
126138
// First version of the record ensure it does not exist
127139
return createInitialRecord(versionAttributeKey);
128140
}
129141
// Existing record, increment version
130-
return updateExistingRecord(versionAttributeKey, existingVersionValue.get());
142+
return updateExistingRecord(versionAttributeKey, existingVersionValue);
131143
}
132144

133-
private boolean isInitialVersion(Optional<AttributeValue> existingVersionValue) {
134-
return !existingVersionValue.isPresent() || isNullAttributeValue(existingVersionValue.get());
145+
private boolean isInitialVersion(AttributeValue existingVersionValue) {
146+
return isNullAttributeValue(existingVersionValue)
147+
|| getExistingVersion(existingVersionValue) == this.startingValue;
135148
}
136149

137150
private Pair<AttributeValue, Expression> createInitialRecord(String versionAttributeKey) {
138-
AttributeValue newVersionValue = incrementVersion(0);
151+
AttributeValue newVersionValue = incrementVersion(this.startingValue);
139152

140153
String attributeKeyRef = keyRef(versionAttributeKey);
141154

@@ -177,16 +190,43 @@ private int getExistingVersion(AttributeValue existingVersionValue) {
177190
}
178191

179192
private AttributeValue incrementVersion(int version) {
180-
return AttributeValue.fromN(Integer.toString(version + 1));
193+
return AttributeValue.fromN(Integer.toString(version + this.increment));
181194
}
182195

183196
@NotThreadSafe
184197
public static final class Builder {
198+
private int startingValue = 0;
199+
private int increment = 1;
200+
185201
private Builder() {
186202
}
187203

204+
/**
205+
* Sets the startingValue used to compare if a record is the initial version of a record.
206+
* Default value - {@code 0}.
207+
*
208+
* @param startingValue
209+
* @return the builder instance
210+
*/
211+
public Builder startAt(int startingValue) {
212+
this.startingValue = startingValue;
213+
return this;
214+
}
215+
216+
/**
217+
* Sets the amount to increment the version by with each subsequent update.
218+
* Default value - {@code 1}.
219+
*
220+
* @param increment
221+
* @return the builder instance
222+
*/
223+
public Builder incrementBy(int increment) {
224+
this.increment = increment;
225+
return this;
226+
}
227+
188228
public VersionedRecordExtension build() {
189-
return new VersionedRecordExtension();
229+
return new VersionedRecordExtension(this.startingValue, this.increment);
190230
}
191231
}
192232
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,33 @@ public void beforeWrite_initialVersionDueToExplicitNull_transformedItemIsCorrect
112112
assertThat(result.transformedItem(), is(fakeItemWithInitialVersion));
113113
}
114114

115+
@Test
116+
public void beforeWrite_initialVersionDueToExplicitZero_expressionAndTransformedItemIsCorrect() {
117+
FakeItem fakeItem = createUniqueFakeItem();
118+
119+
Map<String, AttributeValue> inputMap =
120+
new HashMap<>(FakeItem.getTableSchema().itemToMap(fakeItem, true));
121+
inputMap.put("version", AttributeValue.builder().n("0").build());
122+
123+
Map<String, AttributeValue> fakeItemWithInitialVersion =
124+
new HashMap<>(FakeItem.getTableSchema().itemToMap(fakeItem, true));
125+
fakeItemWithInitialVersion.put("version", AttributeValue.builder().n("1").build());
126+
127+
WriteModification result =
128+
versionedRecordExtension.beforeWrite(DefaultDynamoDbExtensionContext
129+
.builder()
130+
.items(inputMap)
131+
.tableMetadata(FakeItem.getTableMetadata())
132+
.operationContext(PRIMARY_CONTEXT).build());
133+
134+
assertThat(result.transformedItem(), is(fakeItemWithInitialVersion));
135+
assertThat(result.additionalConditionalExpression(),
136+
is(Expression.builder()
137+
.expression("attribute_not_exists(#AMZN_MAPPED_version)")
138+
.expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
139+
.build()));
140+
}
141+
115142
@Test
116143
public void beforeWrite_existingVersion_expressionIsCorrect() {
117144
FakeItem fakeItem = createUniqueFakeItem();

0 commit comments

Comments
 (0)