Skip to content

Commit 0d11269

Browse files
authored
added ability to set startAt to -1 to allow version 0 (#6653)
added ability to set startAt to -1 to allow versioning to begin from 0
1 parent 9446565 commit 0d11269

File tree

4 files changed

+129
-4
lines changed

4 files changed

+129
-4
lines changed

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
3636
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
3737
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
38-
import software.amazon.awssdk.utils.Validate;
3938

4039
/**
4140
* This extension implements optimistic locking on record writes by means of a 'record version number' that is used
@@ -55,6 +54,10 @@
5554
* Then, whenever a record is written the write operation will only succeed if the version number of the record has not
5655
* been modified since it was last read by the application. Every time a new version of the record is successfully
5756
* written to the database, the record version number will be automatically incremented.
57+
* <p>
58+
* <b>Version Calculation:</b> The first version written to a new record is calculated as {@code startAt + incrementBy}.
59+
* For example, with {@code startAt=0} and {@code incrementBy=1} (defaults), the first version is 1.
60+
* To start versioning from 0, use {@code startAt=-1} and {@code incrementBy=1}, which produces first version = 0.
5861
*/
5962
@SdkPublicApi
6063
@ThreadSafe
@@ -68,7 +71,9 @@ public final class VersionedRecordExtension implements DynamoDbEnhancedClientExt
6871
private final long incrementBy;
6972

7073
private VersionedRecordExtension(Long startAt, Long incrementBy) {
71-
Validate.isNotNegativeOrNull(startAt, "startAt");
74+
if (startAt != null && startAt < -1) {
75+
throw new IllegalArgumentException("startAt must be -1 or greater");
76+
}
7277

7378
if (incrementBy != null && incrementBy < 1) {
7479
throw new IllegalArgumentException("incrementBy must be greater than 0.");
@@ -121,7 +126,9 @@ public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName
121126
"is supported.", attributeName, attributeValueType.name()));
122127
}
123128

124-
Validate.isNotNegativeOrNull(startAt, "startAt");
129+
if (startAt != null && startAt < -1) {
130+
throw new IllegalArgumentException("startAt must be -1 or greater.");
131+
}
125132

126133
if (incrementBy != null && incrementBy < 1) {
127134
throw new IllegalArgumentException("incrementBy must be greater than 0.");
@@ -228,9 +235,10 @@ private Builder() {
228235

229236
/**
230237
* Sets the startAt used to compare if a record is the initial version of a record.
238+
* The first version written to a new record is calculated as {@code startAt + incrementBy}.
231239
* Default value - {@code 0}.
232240
*
233-
* @param startAt the starting value for version comparison, must not be negative
241+
* @param startAt the starting value for version comparison, must be -1 or greater
234242
* @return the builder instance
235243
*/
236244
public Builder startAt(Long startAt) {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
* Denotes this attribute as recording the version record number to be used for optimistic locking. Every time a record
2828
* with this attribute is written to the database it will be incremented and a condition added to the request to check
2929
* for an exact match of the old version.
30+
* <p>
31+
* <b>Version Calculation:</b> The first version written to a new record is calculated as {@code startAt + incrementBy}.
32+
* For example, with {@code startAt=0} and {@code incrementBy=1} (defaults), the first version is 1.
33+
* To start versioning from 0, use {@code startAt=-1} and {@code incrementBy=1}, which produces first version = 0.
3034
*/
3135
@SdkPublicApi
3236
@Target({ElementType.METHOD})

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,26 @@ public void updateItem_existingRecordWithVersionEqualToStartAt_shouldSucceed() {
653653
is("attribute_not_exists(#AMZN_MAPPED_version) OR #AMZN_MAPPED_version = :old_version_value"));
654654
}
655655

656+
@Test
657+
public void beforeWrite_startAtNegativeOne_firstVersionIsZero() {
658+
VersionedRecordExtension extension = VersionedRecordExtension.builder()
659+
.startAt(-1L)
660+
.incrementBy(1L)
661+
.build();
662+
FakeItem fakeItem = createUniqueFakeItem();
663+
Map<String, AttributeValue> expectedItem =
664+
new HashMap<>(FakeItem.getTableSchema().itemToMap(fakeItem, true));
665+
expectedItem.put("version", AttributeValue.builder().n("0").build());
666+
667+
WriteModification result =
668+
extension.beforeWrite(DefaultDynamoDbExtensionContext
669+
.builder()
670+
.items(FakeItem.getTableSchema().itemToMap(fakeItem, true))
671+
.tableMetadata(FakeItem.getTableMetadata())
672+
.operationContext(PRIMARY_CONTEXT).build());
673+
674+
assertThat(result.transformedItem(), is(expectedItem));
675+
}
656676

657677
public static Stream<Arguments> customIncrementForExistingVersionValues() {
658678
return Stream.of(

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,24 @@ public AnnotatedRecord setAttribute(String attribute) {
143143
}
144144
}
145145

146+
@DynamoDbBean
147+
public static class AnnotatedRecordStartAtNegativeOne {
148+
private String id;
149+
private String attribute;
150+
private Long version;
151+
152+
@DynamoDbPartitionKey
153+
public String getId() { return id; }
154+
public void setId(String id) { this.id = id; }
155+
156+
@DynamoDbVersionAttribute(startAt = -1, incrementBy = 1)
157+
public Long getVersion() { return version; }
158+
public void setVersion(Long version) { this.version = version; }
159+
160+
public String getAttribute() { return attribute; }
161+
public void setAttribute(String attribute) { this.attribute = attribute; }
162+
}
163+
146164
private DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
147165
.dynamoDbClient(getDynamoDbClient())
148166
.extensions(VersionedRecordExtension.builder().build())
@@ -158,18 +176,37 @@ public AnnotatedRecord setAttribute(String attribute) {
158176
)
159177
.build();
160178

179+
private DynamoDbEnhancedClient startAtNegativeOneClient = DynamoDbEnhancedClient.builder()
180+
.dynamoDbClient(getDynamoDbClient())
181+
.extensions(VersionedRecordExtension
182+
.builder()
183+
.startAt(-1L)
184+
.incrementBy(1L)
185+
.build()
186+
)
187+
.build();
188+
161189
private DynamoDbTable<Record> mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA);
162190

163191
private DynamoDbTable<Record> mappedCustomVersionedTable = customVersionedEnhancedClient
164192
.table(getConcreteTableName("table-name2"), TABLE_SCHEMA);
165193

194+
private DynamoDbTable<Record> startAtNegativeOneTable = startAtNegativeOneClient
195+
.table(getConcreteTableName("startAt-neg-one-table"), TABLE_SCHEMA);
196+
166197

167198
private static final TableSchema<AnnotatedRecord> ANNOTATED_TABLE_SCHEMA =
168199
TableSchema.fromBean(AnnotatedRecord.class);
169200

170201
private DynamoDbTable<AnnotatedRecord> annotatedTable = enhancedClient
171202
.table(getConcreteTableName("annotated-table"), ANNOTATED_TABLE_SCHEMA);
172203

204+
private static final TableSchema<AnnotatedRecordStartAtNegativeOne> ANNOTATED_START_AT_NEG_ONE_SCHEMA =
205+
TableSchema.fromBean(AnnotatedRecordStartAtNegativeOne.class);
206+
207+
private DynamoDbTable<AnnotatedRecordStartAtNegativeOne> annotatedStartAtNegativeOneTable = enhancedClient
208+
.table(getConcreteTableName("annotated-startAt-neg-one-table"), ANNOTATED_START_AT_NEG_ONE_SCHEMA);
209+
173210

174211

175212

@@ -181,6 +218,8 @@ public void createTable() {
181218
mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
182219
mappedCustomVersionedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
183220
annotatedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
221+
startAtNegativeOneTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
222+
annotatedStartAtNegativeOneTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
184223
}
185224

186225
@After
@@ -196,6 +235,14 @@ public void deleteTable() {
196235
getDynamoDbClient().deleteTable(DeleteTableRequest.builder()
197236
.tableName(getConcreteTableName("annotated-table"))
198237
.build());
238+
239+
getDynamoDbClient().deleteTable(DeleteTableRequest.builder()
240+
.tableName(getConcreteTableName("startAt-neg-one-table"))
241+
.build());
242+
243+
getDynamoDbClient().deleteTable(DeleteTableRequest.builder()
244+
.tableName(getConcreteTableName("annotated-startAt-neg-one-table"))
245+
.build());
199246
}
200247

201248

@@ -580,4 +627,50 @@ public void deleteItem_annotationConfigWithVersionEqualToStartAt_shouldSucceed()
580627
AnnotatedRecord shouldBeNull = annotatedTable.getItem(r -> r.key(k -> k.partitionValue("delete-annotation")));
581628
assertThat(shouldBeNull, is(nullValue()));
582629
}
630+
631+
@Test
632+
public void putItem_startAtNegativeOne_firstVersionIsZero() {
633+
startAtNegativeOneTable.putItem(r -> r.item(new Record().setId("test-id").setAttribute("value")));
634+
635+
Record result = startAtNegativeOneTable.getItem(r -> r.key(k -> k.partitionValue("test-id")));
636+
assertThat(result.getVersion(), is(0));
637+
}
638+
639+
@Test
640+
public void updateItem_startAtNegativeOne_incrementsFromZero() {
641+
startAtNegativeOneTable.putItem(r -> r.item(new Record().setId("test-id-2").setAttribute("value")));
642+
643+
Record recordToUpdate = startAtNegativeOneTable.getItem(r -> r.key(k -> k.partitionValue("test-id-2")));
644+
recordToUpdate.setAttribute("updated");
645+
startAtNegativeOneTable.updateItem(r -> r.item(recordToUpdate));
646+
647+
Record result = startAtNegativeOneTable.getItem(r -> r.key(k -> k.partitionValue("test-id-2")));
648+
assertThat(result.getVersion(), is(1));
649+
}
650+
651+
@Test
652+
public void updateItem_startAtNegativeOne_versionMatchesStartAt_shouldSucceed() {
653+
Map<String, AttributeValue> item = new HashMap<>();
654+
item.put("id", stringValue("test-id-3"));
655+
item.put("attribute", stringValue("value"));
656+
item.put("version", AttributeValue.builder().n("-1").build());
657+
getDynamoDbClient().putItem(r -> r.tableName(startAtNegativeOneTable.tableName()).item(item));
658+
659+
startAtNegativeOneTable.updateItem(r -> r.item(new Record().setId("test-id-3").setAttribute("updated").setVersion(-1)));
660+
661+
Record result = startAtNegativeOneTable.getItem(r -> r.key(k -> k.partitionValue("test-id-3")));
662+
assertThat(result.getVersion(), is(0));
663+
}
664+
665+
@Test
666+
public void annotatedRecord_startAtNegativeOne_firstVersionIsZero() {
667+
AnnotatedRecordStartAtNegativeOne record = new AnnotatedRecordStartAtNegativeOne();
668+
record.setId("test-id");
669+
record.setAttribute("value");
670+
671+
annotatedStartAtNegativeOneTable.putItem(record);
672+
673+
AnnotatedRecordStartAtNegativeOne result = annotatedStartAtNegativeOneTable.getItem(r -> r.key(k -> k.partitionValue("test-id")));
674+
assertThat(result.getVersion(), is(0L));
675+
}
583676
}

0 commit comments

Comments
 (0)