Skip to content

DDB Enhanced: Allow custom versioning #6019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactoring and adding test coverage
  • Loading branch information
RanVaknin committed May 6, 2025
commit b7b79c25ddc6ef6885311f8407a0cd5984167862
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"type": "feature",
"category": "DynamoDB Enhanced Client",
"contributor": "akiesler",
"description": "DynamoDB Enhanced Client: Support for Version Starting at 0 with Configurable Increment"
"description": "Support for Version Starting at 0 with Configurable Increment"
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@
<includeModule>polly</includeModule>
</includeModules>
<excludes>
<!-- TODO remove after release -->
<exclude>software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute#incrementBy()</exclude>
<exclude>software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute#startAt()</exclude>
<exclude>*.internal.*</exclude>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.utils.ToString;

/**
* High-level representation of a DynamoDB 'expression' that can be used in various situations where the API requires
Expand Down Expand Up @@ -313,11 +314,16 @@ public int hashCode() {

@Override
public String toString() {
return "Expression{" +
"expression='" + expression + '\'' +
", expressionValues=" + expressionValues +
", expressionNames=" + expressionNames +
'}';
// return "Expression{" +
// "expression='" + expression + '\'' +
// ", expressionValues=" + expressionValues +
// ", expressionNames=" + expressionNames +
// '}';
return ToString.builder("Expression")
.add("expression", expression)
.add("expressionValues", expressionValues)
.add("expressionNames", expressionNames)
.build();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private VersionedRecordExtension(Long startAt, Long incrementBy) {
Validate.isNotNegativeOrNull(startAt, "startAt");

if (incrementBy != null && incrementBy < 1) {
throw new IllegalArgumentException("IncrementBy must be greater than 0.");
throw new IllegalArgumentException("incrementBy must be greater than 0.");
}

this.startAt = startAt != null ? startAt : 0L;
Expand Down Expand Up @@ -156,13 +156,7 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
Optional<Long> 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))) {

if (isInitialVersion(existingVersionValue, versionStartAtFromAnnotation)) {
long startValue = versionStartAtFromAnnotation.orElse(this.startAt);
long increment = versionIncrementByFromAnnotation.orElse(this.incrementBy);

Expand Down Expand Up @@ -200,10 +194,25 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
.build();
}

private boolean isInitialVersion(Optional<AttributeValue> existingVersionValue, Optional<Long> versionStartAtFromAnnotation) {
if (!existingVersionValue.isPresent() || isNullAttributeValue(existingVersionValue.get())) {
return true;
}

AttributeValue value = existingVersionValue.get();
if (value.n() != null) {
long currentVersion = Long.parseLong(value.n());
return (versionStartAtFromAnnotation.isPresent() && currentVersion == versionStartAtFromAnnotation.get())
|| currentVersion == this.startAt;
}

return false;
}

@NotThreadSafe
public static final class Builder {
private Long startAt = 0L;
private Long incrementBy = 1L;
private Long startAt;
private Long incrementBy;

private Builder() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@
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.AttributeConverterProvider;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
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.document.DocumentTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument;
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.functionaltests.models.ImmutableFakeVersionedItem;
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;
Expand Down Expand Up @@ -511,6 +516,44 @@ public void customIncrementForExistingVersion_worksAsExpected(Long startAt, Long
is("#AMZN_MAPPED_version = :old_version_value"));
}

@ParameterizedTest
@MethodSource("customIncrementForExistingVersionValues")
public void customIncrementForExistingVersion_withImmutableSchema_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();

ImmutableFakeVersionedItem fakeItem = ImmutableFakeVersionedItem
.builder()
.id(UUID.randomUUID().toString())
.version(existingVersion)
.build();

Map<String, AttributeValue> inputMap =
new HashMap<>(ImmutableFakeVersionedItem.getTableSchema().itemToMap(fakeItem, true));

Map<String, AttributeValue> expectedVersionedItem =
new HashMap<>(ImmutableFakeVersionedItem.getTableSchema().itemToMap(fakeItem, true));
expectedVersionedItem.put("version", AttributeValue.builder().n(expectedNextVersion).build());

WriteModification result =
recordExtension.beforeWrite(DefaultDynamoDbExtensionContext
.builder()
.items(inputMap)
.tableMetadata(ImmutableFakeVersionedItem.getTableMetadata())
.operationContext(PRIMARY_CONTEXT).build());

assertThat(result.transformedItem(), is(expectedVersionedItem));
assertThat(result.additionalConditionalExpression().expression(),
is("#AMZN_MAPPED_version = :old_version_value"));
}

public static Stream<Arguments> customIncrementForExistingVersionValues() {
return Stream.of(
Arguments.of(0L, 1L, 5L, "6"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models;

import static software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension.AttributeTags.versionAttribute;
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey;

import java.util.Objects;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;

@DynamoDbImmutable(builder = ImmutableFakeVersionedItem.Builder.class)
public class ImmutableFakeVersionedItem {
private final String id;
private final String attribute;
private final long version;

private ImmutableFakeVersionedItem(Builder b) {
this.id = b.id;
this.attribute = b.attribute;
this.version = b.version;
}

public static Builder builder() {
return new Builder();
}

public String attribute() {
return attribute;
}

@DynamoDbPartitionKey
public String id() {
return id;
}

public long version() {
return version;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ImmutableFakeVersionedItem that = (ImmutableFakeVersionedItem) o;
return version == that.version && Objects.equals(id, that.id) && Objects.equals(attribute, that.attribute);
}

@Override
public int hashCode() {
return Objects.hash(id, attribute, version);
}

public static TableSchema<ImmutableFakeVersionedItem> getTableSchema() {
return StaticImmutableTableSchema.builder(ImmutableFakeVersionedItem.class, ImmutableFakeVersionedItem.Builder.class)
.newItemBuilder(ImmutableFakeVersionedItem::builder, ImmutableFakeVersionedItem.Builder::build)
.addAttribute(String.class, a -> a.name("id")
.getter(ImmutableFakeVersionedItem::id)
.setter(ImmutableFakeVersionedItem.Builder::id)
.tags(primaryPartitionKey()))
.addAttribute(Long.class, a -> a.name("version")
.getter(ImmutableFakeVersionedItem::version)
.setter(ImmutableFakeVersionedItem.Builder::version)
.tags(versionAttribute()))
.addAttribute(String.class, a -> a.name("attribute")
.getter(ImmutableFakeVersionedItem::attribute)
.setter(ImmutableFakeVersionedItem.Builder::attribute))
.build();
}




public static TableMetadata getTableMetadata() {
return getTableSchema().tableMetadata();
}

public static final class Builder {
private String id;
private String attribute;
private long version;

public Builder id(String id) {
this.id = id;
return this;
}

public Builder version(long version) {
this.version = version;
return this;
}

public Builder attribute(String attribute) {
this.attribute = attribute;
return this;
}

public ImmutableFakeVersionedItem build() {
return new ImmutableFakeVersionedItem(this);
}
}
}
Loading