diff --git a/.changes/next-release/feature-DynamoDBEnhancedClient-2a501d8.json b/.changes/next-release/feature-DynamoDBEnhancedClient-2a501d8.json
new file mode 100644
index 000000000000..cfb75bbcf67e
--- /dev/null
+++ b/.changes/next-release/feature-DynamoDBEnhancedClient-2a501d8.json
@@ -0,0 +1,6 @@
+{
+ "type": "feature",
+ "category": "DynamoDB Enhanced Client",
+ "contributor": "akiesler",
+ "description": "DynamoDB Enhanced Client: Support for Version Starting at 0 with Configurable Increment"
+}
diff --git a/pom.xml b/pom.xml
index 92cc17a8ab3f..70cba84d3421 100644
--- a/pom.xml
+++ b/pom.xml
@@ -679,6 +679,8 @@
polly
+ software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute#incrementBy()
+ software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute#startAt()
*.internal.*
software.amazon.awssdk.thirdparty.*
software.amazon.awssdk.regions.*
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/Expression.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/Expression.java
index fa0f69ad9ed3..5a4d9e454e47 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/Expression.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/Expression.java
@@ -311,6 +311,15 @@ public int hashCode() {
return result;
}
+ @Override
+ public String toString() {
+ return "Expression{" +
+ "expression='" + expression + '\'' +
+ ", expressionValues=" + expressionValues +
+ ", expressionNames=" + expressionNames +
+ '}';
+ }
+
/**
* A builder for {@link Expression}
*/
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtension.java
index 34a6396c5109..89bb11aa31df 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtension.java
@@ -34,6 +34,7 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.utils.Validate;
/**
* This extension implements optimistic locking on record writes by means of a 'record version number' that is used
@@ -61,7 +62,18 @@ public final class VersionedRecordExtension implements DynamoDbEnhancedClientExt
private static final String CUSTOM_METADATA_KEY = "VersionedRecordExtension:VersionAttribute";
private static final VersionAttribute VERSION_ATTRIBUTE = new VersionAttribute();
- private VersionedRecordExtension() {
+ private final long startAt;
+ private final long incrementBy;
+
+ private VersionedRecordExtension(Long startAt, Long incrementBy) {
+ Validate.isNotNegativeOrNull(startAt, "startAt");
+
+ if (incrementBy != null && incrementBy < 1) {
+ throw new IllegalArgumentException("IncrementBy must be greater than 0.");
+ }
+
+ this.startAt = startAt != null ? startAt : 0L;
+ this.incrementBy = incrementBy != null ? incrementBy : 1L;
}
public static Builder builder() {
@@ -75,19 +87,47 @@ private AttributeTags() {
public static StaticAttributeTag versionAttribute() {
return VERSION_ATTRIBUTE;
}
+
+ public static StaticAttributeTag versionAttribute(Long startAt, Long incrementBy) {
+ return new VersionAttribute(startAt, incrementBy);
+ }
}
- private static class VersionAttribute implements StaticAttributeTag {
+ private static final class VersionAttribute implements StaticAttributeTag {
+ private static final String START_AT_METADATA_KEY = "VersionedRecordExtension:StartAt";
+ private static final String INCREMENT_BY_METADATA_KEY = "VersionedRecordExtension:IncrementBy";
+
+ private final Long startAt;
+ private final Long incrementBy;
+
+ private VersionAttribute() {
+ this.startAt = null;
+ this.incrementBy = null;
+ }
+
+ private VersionAttribute(Long startAt, Long incrementBy) {
+ this.startAt = startAt;
+ this.incrementBy = incrementBy;
+ }
+
@Override
public Consumer modifyMetadata(String attributeName,
AttributeValueType attributeValueType) {
if (attributeValueType != AttributeValueType.N) {
throw new IllegalArgumentException(String.format(
"Attribute '%s' of type %s is not a suitable type to be used as a version attribute. Only type 'N' " +
- "is supported.", attributeName, attributeValueType.name()));
+ "is supported.", attributeName, attributeValueType.name()));
+ }
+
+ Validate.isNotNegativeOrNull(startAt, "startAt");
+
+ if (incrementBy != null && incrementBy < 1) {
+ throw new IllegalArgumentException("IncrementBy must be greater than 0.");
}
return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, attributeName)
+ .addCustomMetadataObject(START_AT_METADATA_KEY, startAt)
+ .addCustomMetadataObject(INCREMENT_BY_METADATA_KEY, incrementBy)
.markAttributeAsKey(attributeName, attributeValueType);
}
}
@@ -109,9 +149,24 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
Optional existingVersionValue =
Optional.ofNullable(itemToTransform.get(versionAttributeKey.get()));
- if (!existingVersionValue.isPresent() || isNullAttributeValue(existingVersionValue.get())) {
- // First version of the record
- newVersionValue = AttributeValue.builder().n("1").build();
+ Optional versionStartAtFromAnnotation = context.tableMetadata()
+ .customMetadataObject(VersionAttribute.START_AT_METADATA_KEY,
+ Long.class);
+
+ Optional versionIncrementByFromAnnotation = context.tableMetadata()
+ .customMetadataObject(VersionAttribute.INCREMENT_BY_METADATA_KEY,
+ Long.class);
+
+ if (!existingVersionValue.isPresent() || isNullAttributeValue(existingVersionValue.get()) ||
+ (existingVersionValue.get().n() != null &&
+ ((versionStartAtFromAnnotation.isPresent() &&
+ Long.parseLong(existingVersionValue.get().n()) == versionStartAtFromAnnotation.get()) ||
+ Long.parseLong(existingVersionValue.get().n()) == this.startAt))) {
+
+ long startValue = versionStartAtFromAnnotation.orElse(this.startAt);
+ long increment = versionIncrementByFromAnnotation.orElse(this.incrementBy);
+
+ newVersionValue = AttributeValue.builder().n(Long.toString(startValue + increment)).build();
condition = Expression.builder()
.expression(String.format("attribute_not_exists(%s)", attributeKeyRef))
.expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeKey.get()))
@@ -123,9 +178,12 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
throw new IllegalArgumentException("Version attribute appears to be the wrong type. N is required.");
}
- int existingVersion = Integer.parseInt(existingVersionValue.get().n());
+ long existingVersion = Long.parseLong(existingVersionValue.get().n());
String existingVersionValueKey = VERSIONED_RECORD_EXPRESSION_VALUE_KEY_MAPPER.apply(versionAttributeKey.get());
- newVersionValue = AttributeValue.builder().n(Integer.toString(existingVersion + 1)).build();
+
+ long increment = versionIncrementByFromAnnotation.orElse(this.incrementBy);
+ newVersionValue = AttributeValue.builder().n(Long.toString(existingVersion + increment)).build();
+
condition = Expression.builder()
.expression(String.format("%s = %s", attributeKeyRef, existingVersionValueKey))
.expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeKey.get()))
@@ -144,11 +202,38 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
@NotThreadSafe
public static final class Builder {
+ private Long startAt = 0L;
+ private Long incrementBy = 1L;
+
private Builder() {
}
+ /**
+ * Sets the startAt used to compare if a record is the initial version of a record.
+ * Default value - {@code 0}.
+ *
+ * @param startAt
+ * @return the builder instance
+ */
+ public Builder startAt(Long startAt) {
+ this.startAt = startAt;
+ return this;
+ }
+
+ /**
+ * Sets the amount to increment the version by with each subsequent update.
+ * Default value - {@code 1}.
+ *
+ * @param incrementBy
+ * @return the builder instance
+ */
+ public Builder incrementBy(Long incrementBy) {
+ this.incrementBy = incrementBy;
+ return this;
+ }
+
public VersionedRecordExtension build() {
- return new VersionedRecordExtension();
+ return new VersionedRecordExtension(this.startAt, this.incrementBy);
}
}
}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbVersionAttribute.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbVersionAttribute.java
index 21f3beeeb446..09ab6eb00159 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbVersionAttribute.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/annotations/DynamoDbVersionAttribute.java
@@ -33,4 +33,20 @@
@Retention(RetentionPolicy.RUNTIME)
@BeanTableSchemaAttributeTag(VersionRecordAttributeTags.class)
public @interface DynamoDbVersionAttribute {
+ /**
+ * The starting value for the version attribute.
+ * Default value - {@code 0}.
+ *
+ * @return the starting value
+ */
+ long startAt() default 0;
+
+ /**
+ * The amount to increment the version by with each update.
+ * Default value - {@code 1}.
+ *
+ * @return the increment value
+ */
+ long incrementBy() default 1;
+
}
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/VersionRecordAttributeTags.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/VersionRecordAttributeTags.java
index e1c2d527866b..d81cf268afff 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/VersionRecordAttributeTags.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/extensions/VersionRecordAttributeTags.java
@@ -26,6 +26,6 @@ private VersionRecordAttributeTags() {
}
public static StaticAttributeTag attributeTagFor(DynamoDbVersionAttribute annotation) {
- return VersionedRecordExtension.AttributeTags.versionAttribute();
+ return VersionedRecordExtension.AttributeTags.versionAttribute(annotation.startAt(), annotation.incrementBy());
}
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java
index 4f61db7487e9..2dac494b943c 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/extensions/VersionedRecordExtensionTest.java
@@ -18,19 +18,29 @@
import static java.util.Collections.singletonMap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem.createUniqueFakeItem;
import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort.createUniqueFakeItemWithSort;
import java.util.HashMap;
import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Stream;
import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.awssdk.enhanced.dynamodb.Expression;
import software.amazon.awssdk.enhanced.dynamodb.OperationContext;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute;
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem;
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort;
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
public class VersionedRecordExtensionTest {
@@ -47,10 +57,10 @@ public void beforeRead_doesNotTransformObject() {
ReadModification result =
versionedRecordExtension.afterRead(DefaultDynamoDbExtensionContext
- .builder()
- .items(fakeItemMap)
- .tableMetadata(FakeItem.getTableMetadata())
- .operationContext(PRIMARY_CONTEXT).build());
+ .builder()
+ .items(fakeItemMap)
+ .tableMetadata(FakeItem.getTableMetadata())
+ .operationContext(PRIMARY_CONTEXT).build());
assertThat(result, is(ReadModification.builder().build()));
}
@@ -168,15 +178,344 @@ public void beforeWrite_returnsNoOpModification_ifVersionAttributeNotDefined() {
@Test(expected = IllegalArgumentException.class)
public void beforeWrite_throwsIllegalArgumentException_ifVersionAttributeIsWrongType() {
FakeItem fakeItem = createUniqueFakeItem();
- Map fakeItemWIthBadVersion =
+ Map fakeItemWithBadVersion =
new HashMap<>(FakeItem.getTableSchema().itemToMap(fakeItem, true));
- fakeItemWIthBadVersion.put("version", AttributeValue.builder().s("14").build());
+ fakeItemWithBadVersion.put("version", AttributeValue.builder().s("14").build());
versionedRecordExtension.beforeWrite(
DefaultDynamoDbExtensionContext.builder()
- .items(fakeItemWIthBadVersion)
+ .items(fakeItemWithBadVersion)
.operationContext(PRIMARY_CONTEXT)
.tableMetadata(FakeItem.getTableMetadata())
.build());
}
+
+ @Test
+ public void beforeWrite_versionEqualsStartAt_treatedAsInitialVersion() {
+ VersionedRecordExtension recordExtension = VersionedRecordExtension.builder()
+ .startAt(5L)
+ .build();
+
+ FakeItem fakeItem = createUniqueFakeItem();
+ fakeItem.setVersion(5);
+
+ Map inputMap =
+ new HashMap<>(FakeItem.getTableSchema().itemToMap(fakeItem, true));
+
+ WriteModification result =
+ recordExtension.beforeWrite(DefaultDynamoDbExtensionContext
+ .builder()
+ .items(inputMap)
+ .tableMetadata(FakeItem.getTableMetadata())
+ .operationContext(PRIMARY_CONTEXT).build());
+
+ assertThat(result.additionalConditionalExpression().expression(),
+ is("attribute_not_exists(#AMZN_MAPPED_version)"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("customStartAtAndIncrementValues")
+ public void customStartingValueAndIncrement_worksAsExpected(Long startAt, Long incrementBy, String expectedVersion) {
+ VersionedRecordExtension.Builder recordExtensionBuilder = VersionedRecordExtension.builder();
+ if (startAt != null) {
+ recordExtensionBuilder.startAt(startAt);
+ }
+ if (incrementBy != null) {
+ recordExtensionBuilder.incrementBy(incrementBy);
+ }
+
+ VersionedRecordExtension recordExtension = recordExtensionBuilder.build();
+
+ FakeItem fakeItem = createUniqueFakeItem();
+
+ Map inputMap =
+ new HashMap<>(FakeItem.getTableSchema().itemToMap(fakeItem, true));
+
+ Map expectedInitialVersion =
+ new HashMap<>(FakeItem.getTableSchema().itemToMap(fakeItem, true));
+
+ expectedInitialVersion.put("version", AttributeValue.builder().n(expectedVersion).build());
+
+ WriteModification result =
+ recordExtension.beforeWrite(DefaultDynamoDbExtensionContext
+ .builder()
+ .items(inputMap)
+ .tableMetadata(FakeItem.getTableMetadata())
+ .operationContext(PRIMARY_CONTEXT).build());
+
+ assertThat(result.transformedItem(), is(expectedInitialVersion));
+ assertThat(result.additionalConditionalExpression(),
+ is(Expression.builder()
+ .expression("attribute_not_exists(#AMZN_MAPPED_version)")
+ .expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
+ .build()));
+ }
+
+ public static Stream customStartAtAndIncrementValues() {
+ return Stream.of(
+ Arguments.of(0L,1L,"1"),
+ Arguments.of(3L,2L,"5"),
+ Arguments.of(3L,null,"4"),
+ Arguments.of(null,3L,"3"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("customFailingStartAtAndIncrementValues")
+ public void customStartingValueAndIncrement_shouldThrow(Long startAt, Long incrementBy) {
+ assertThrows(IllegalArgumentException.class, () -> VersionedRecordExtension.builder()
+ .startAt(startAt)
+ .incrementBy(incrementBy)
+ .build());
+ }
+
+ public static Stream customFailingStartAtAndIncrementValues() {
+ return Stream.of(
+ Arguments.of(-2L, 1L),
+ Arguments.of(3L, 0L));
+ }
+
+ @Test
+ public void beforeWrite_versionNotEqualsAnnotationStartAt_notTreatedAsInitialVersion() {
+ FakeVersionedThroughAnnotationItem item = new FakeVersionedThroughAnnotationItem();
+ item.setId(UUID.randomUUID().toString());
+ item.setVersion(10L);
+
+ TableSchema schema =
+ TableSchema.fromBean(FakeVersionedThroughAnnotationItem.class);
+
+ Map inputMap = new HashMap<>(schema.itemToMap(item, true));
+
+ VersionedRecordExtension recordExtension = VersionedRecordExtension.builder().build();
+
+ WriteModification result =
+ recordExtension.beforeWrite(DefaultDynamoDbExtensionContext
+ .builder()
+ .items(inputMap)
+ .tableMetadata(schema.tableMetadata())
+ .operationContext(PRIMARY_CONTEXT).build());
+
+ assertThat(result.additionalConditionalExpression().expression(),
+ is("#AMZN_MAPPED_version = :old_version_value"));
+ }
+
+ @Test
+ public void beforeWrite_versionEqualsAnnotationStartAt_isTreatedAsInitialVersion() {
+ FakeVersionedThroughAnnotationItem item = new FakeVersionedThroughAnnotationItem();
+ item.setId(UUID.randomUUID().toString());
+ item.setVersion(3L);
+
+ TableSchema schema =
+ TableSchema.fromBean(FakeVersionedThroughAnnotationItem.class);
+
+ Map inputMap = new HashMap<>(schema.itemToMap(item, true));
+
+ VersionedRecordExtension recordExtension = VersionedRecordExtension.builder().build();
+
+ WriteModification result =
+ recordExtension.beforeWrite(DefaultDynamoDbExtensionContext
+ .builder()
+ .items(inputMap)
+ .tableMetadata(schema.tableMetadata())
+ .operationContext(PRIMARY_CONTEXT).build());
+
+ assertThat(result.additionalConditionalExpression().expression(),
+ is("attribute_not_exists(#AMZN_MAPPED_version)"));
+ }
+
+
+ @DynamoDbBean
+ public static class FakeVersionedThroughAnnotationItem {
+ private String id;
+ private Long version;
+
+ public FakeVersionedThroughAnnotationItem() {
+ }
+
+ @DynamoDbPartitionKey
+ public String getId() { return id; }
+ public void setId(String id) { this.id = id; }
+
+ @DynamoDbVersionAttribute(startAt = 3, incrementBy = 2)
+ public Long getVersion() { return version; }
+ public void setVersion(Long version) { this.version = version; }
+ }
+
+
+ @Test
+ public void customStartingValueAndIncrementWithAnnotation_worksAsExpected() {
+ VersionedRecordExtension recordExtension = VersionedRecordExtension.builder().build();
+
+ FakeVersionedThroughAnnotationItem item = new FakeVersionedThroughAnnotationItem();
+ item.setId(UUID.randomUUID().toString());
+
+ TableSchema schema = TableSchema.fromBean(FakeVersionedThroughAnnotationItem.class);
+
+ Map inputMap = new HashMap<>(schema.itemToMap(item, true));
+
+ Map expectedInitialVersion = new HashMap<>(schema.itemToMap(item, true));
+ expectedInitialVersion.put("version", AttributeValue.builder().n("5").build());
+
+ WriteModification result =
+ recordExtension.beforeWrite(DefaultDynamoDbExtensionContext
+ .builder()
+ .items(inputMap)
+ .tableMetadata(schema.tableMetadata())
+ .operationContext(PRIMARY_CONTEXT).build());
+
+ assertThat(result.transformedItem(), is(expectedInitialVersion));
+ assertThat(result.additionalConditionalExpression(),
+ is(Expression.builder()
+ .expression("attribute_not_exists(#AMZN_MAPPED_version)")
+ .expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
+ .build()));
+ }
+
+ @Test
+ public void customAnnotationValuesAndBuilderValues_annotationShouldTakePrecedence() {
+ VersionedRecordExtension recordExtension = VersionedRecordExtension.builder()
+ .startAt(5L)
+ .incrementBy(2L)
+ .build();
+
+ FakeVersionedThroughAnnotationItem item = new FakeVersionedThroughAnnotationItem();
+ item.setId(UUID.randomUUID().toString());
+
+ TableSchema schema = TableSchema.fromBean(FakeVersionedThroughAnnotationItem.class);
+
+ Map inputMap = new HashMap<>(schema.itemToMap(item, true));
+
+ Map expectedInitialVersion = new HashMap<>(schema.itemToMap(item, true));
+ expectedInitialVersion.put("version", AttributeValue.builder().n("5").build());
+
+ WriteModification result =
+ recordExtension.beforeWrite(DefaultDynamoDbExtensionContext
+ .builder()
+ .items(inputMap)
+ .tableMetadata(schema.tableMetadata())
+ .operationContext(PRIMARY_CONTEXT).build());
+
+ assertThat(result.transformedItem(), is(expectedInitialVersion));
+ assertThat(result.additionalConditionalExpression(),
+ is(Expression.builder()
+ .expression("attribute_not_exists(#AMZN_MAPPED_version)")
+ .expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
+ .build()));
+ }
+
+ @DynamoDbBean
+ public static class FakeVersionedThroughAnnotationItemWithExplicitDefaultValues {
+ private String id;
+ private Long version;
+
+ public FakeVersionedThroughAnnotationItemWithExplicitDefaultValues() {
+ }
+
+ @DynamoDbPartitionKey
+ public String getId() { return id; }
+ public void setId(String id) { this.id = id; }
+
+ @DynamoDbVersionAttribute(startAt = 0, incrementBy = 1)
+ public Long getVersion() { return version; }
+ public void setVersion(Long version) { this.version = version; }
+ }
+
+ @Test
+ public void customAnnotationDefaultValuesAndBuilderValues_annotationShouldTakePrecedence() {
+ VersionedRecordExtension recordExtension = VersionedRecordExtension.builder()
+ .startAt(5L)
+ .incrementBy(2L)
+ .build();
+
+ FakeVersionedThroughAnnotationItemWithExplicitDefaultValues item = new FakeVersionedThroughAnnotationItemWithExplicitDefaultValues();
+ item.setId(UUID.randomUUID().toString());
+
+ TableSchema schema = TableSchema.fromBean(FakeVersionedThroughAnnotationItemWithExplicitDefaultValues.class);
+
+ Map inputMap = new HashMap<>(schema.itemToMap(item, true));
+
+ Map expectedInitialVersion = new HashMap<>(schema.itemToMap(item, true));
+ expectedInitialVersion.put("version", AttributeValue.builder().n("1").build());
+
+ WriteModification result =
+ recordExtension.beforeWrite(DefaultDynamoDbExtensionContext
+ .builder()
+ .items(inputMap)
+ .tableMetadata(schema.tableMetadata())
+ .operationContext(PRIMARY_CONTEXT).build());
+
+ assertThat(result.transformedItem(), is(expectedInitialVersion));
+ assertThat(result.additionalConditionalExpression(),
+ is(Expression.builder()
+ .expression("attribute_not_exists(#AMZN_MAPPED_version)")
+ .expressionNames(singletonMap("#AMZN_MAPPED_version", "version"))
+ .build()));
+ }
+
+ @DynamoDbBean
+ public static class FakeVersionedThroughAnnotationItemWithInvalidValues {
+ private String id;
+ private Long version;
+
+ public FakeVersionedThroughAnnotationItemWithInvalidValues() {
+ }
+
+ @DynamoDbPartitionKey
+ public String getId() { return id; }
+ public void setId(String id) { this.id = id; }
+
+ @DynamoDbVersionAttribute(startAt = -1, incrementBy = -1)
+ public Long getVersion() { return version; }
+ public void setVersion(Long version) { this.version = version; }
+ }
+
+ @Test
+ public void invalidAnnotationValues_shouldThrowException() {
+ FakeVersionedThroughAnnotationItemWithInvalidValues item = new FakeVersionedThroughAnnotationItemWithInvalidValues();
+ item.setId(UUID.randomUUID().toString());
+
+ assertThrows(IllegalArgumentException.class, () -> TableSchema.fromBean(FakeVersionedThroughAnnotationItemWithInvalidValues.class));
+ }
+
+ @ParameterizedTest
+ @MethodSource("customIncrementForExistingVersionValues")
+ public void customIncrementForExistingVersion_worksAsExpected(Long startAt, Long incrementBy,
+ Long existingVersion, String expectedNextVersion) {
+ VersionedRecordExtension.Builder recordExtensionBuilder = VersionedRecordExtension.builder();
+ if (startAt != null) {
+ recordExtensionBuilder.startAt(startAt);
+ }
+ if (incrementBy != null) {
+ recordExtensionBuilder.incrementBy(incrementBy);
+ }
+ VersionedRecordExtension recordExtension = recordExtensionBuilder.build();
+
+ FakeItem fakeItem = createUniqueFakeItem();
+ fakeItem.setVersion(existingVersion.intValue());
+
+ Map inputMap =
+ new HashMap<>(FakeItem.getTableSchema().itemToMap(fakeItem, true));
+
+ Map expectedVersionedItem =
+ new HashMap<>(FakeItem.getTableSchema().itemToMap(fakeItem, true));
+ expectedVersionedItem.put("version", AttributeValue.builder().n(expectedNextVersion).build());
+
+ WriteModification result =
+ recordExtension.beforeWrite(DefaultDynamoDbExtensionContext
+ .builder()
+ .items(inputMap)
+ .tableMetadata(FakeItem.getTableMetadata())
+ .operationContext(PRIMARY_CONTEXT).build());
+
+ assertThat(result.transformedItem(), is(expectedVersionedItem));
+ assertThat(result.additionalConditionalExpression().expression(),
+ is("#AMZN_MAPPED_version = :old_version_value"));
+ }
+
+ public static Stream customIncrementForExistingVersionValues() {
+ return Stream.of(
+ Arguments.of(0L, 1L, 5L, "6"),
+ Arguments.of(3L, 2L, 7L, "9"),
+ Arguments.of(3L, null, 10L, "11"),
+ Arguments.of(null, 3L, 4L, "7"));
+ }
}