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 8 commits into
base: master
Choose a base branch
from

Conversation

RanVaknin
Copy link
Contributor

@RanVaknin RanVaknin commented Apr 9, 2025

Motivation and Context

This PR builds on the work started in issue #3894 where the VersionedRecordExtension didn't support starting version numbers at 0, requiring clients to use Integer objects (with null initial values) rather than long primitives. As mentioned by @akiesler in the original issue, developers might expect versions to start at 0 and increment from there, rather than having a special case where the value must be initialized to null.

This implementation allows the extension to be more flexible by allowing:

  • Starting versions at 0 (or any non-negative long)
  • Configuring custom increment values (only positive numbers)
  • Supporting these configs both at the extension level and via annotations

Modifications

  • Refactored the VersionedRecordExtension to support explicit startAt and incrementBy values via builder methods (making it opt-in).
  • Expanded the DynamoDbVersionAttribute annotation to support startAt and incrementBy parameters
  • Added validation to prevent negative startAt values and non-positive incrementBy values

Testing

  • Custom startAt and incrementBy values through both builder and annotations
  • Validation of illegal values (negative startAt, zero/negative incrementBy)
  • Precedence rules between annotation and builder configurations
  • Version incrementation for both initial and existing records
  • Edge cases with different configuration combinations

@RanVaknin RanVaknin requested a review from a team as a code owner April 9, 2025 22:19
@RanVaknin RanVaknin force-pushed the rvaknin/ddb-enhanced-client-versioned-record-custom-startAt branch from abcd87a to 4e82d7e Compare April 10, 2025 22:54
<configuration>
<parameter>
<excludes>
<!-- japicmp flagging these incorrectly as a breaking change -->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is odd, can you paste the error logs? If we have to add exclusion, we should add it to the top level pom https://github.com/aws/aws-sdk-java-v2/blob/master/pom.xml#L681

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the error only was shown on CI, but I was able to raise with running mvn japicmp:cmp -pl :dynamodb-enhanced

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.557 s
[INFO] Finished at: 2025-04-21T11:29:14-07:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal com.github.siom79.japicmp:japicmp-maven-plugin:0.15.6:cmp (default-cli) on project dynamodb-enhanced: There is at least one incompatibility: software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute.incrementBy():METHOD_ABSTRACT_ADDED_TO_CLASS,software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute.startAt():METHOD_ABSTRACT_ADDED_TO_CLASS -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

@akiesler
Copy link
Contributor

Thank you for picking up this change! It will greatly simplify our version handling. Please let me know if I can do anything to help.

@RanVaknin RanVaknin force-pushed the rvaknin/ddb-enhanced-client-versioned-record-custom-startAt branch from 9e39a83 to 3d505a6 Compare April 21, 2025 23:42
"type": "feature",
"category": "DynamoDB Enhanced Client",
"contributor": "akiesler",
"description": "DynamoDB Enhanced Client: Support for Version Starting at 0 with Configurable Increment"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Don't need the DynamoDB Enhanced Client: in the description since it's already a category

@@ -679,6 +679,8 @@
<includeModule>polly</includeModule>
</includeModules>
<excludes>
<exclude>software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute#incrementBy()</exclude>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a TODO and create a tracking task to remove this after release

Comment on lines +316 to +322
return "Expression{" +
"expression='" + expression + '\'' +
", expressionValues=" + expressionValues +
", expressionNames=" + expressionNames +
'}';
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use ToString.builder() so it's consistent with the other toString methods in DDB enhanced:

return ToString.builder("EnhancedAttributeValue")
.add("type", type)
.add("value", value)
.build();
}

Validate.isNotNegativeOrNull(startAt, "startAt");

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: technically should match the param name, so should be lowercase "i"

Comment on lines +160 to +164
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))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This is a very complex expression. Let's move this to a separate function so it's easier to reason about and we can document it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. Why do we need to check existingVersionValue.get().n() != null && ((versionStartAtFromAnnotation.isPresent() && Long.parseLong(existingVersionValue.get().n()) == versionStartAtFromAnnotation.get()) || Long.parseLong(existingVersionValue.get().n()) == this.startAt)))?

Suggesting the following

if (existingValueNotAvailable(existingAttributeValue)) {
    long startVersion = versionStartAtFromAnnotation.orElse(this.startAt);
    long incrementVersion = versionIncrementByFromAnnotation.orElse(this.incrementBy);
  ...
} else {
  ....
}

newVersionValue = AttributeValue.builder().n(Integer.toString(existingVersion + 1)).build();

long increment = versionIncrementByFromAnnotation.orElse(this.incrementBy);
newVersionValue = AttributeValue.builder().n(Long.toString(existingVersion + increment)).build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like the original version handled this, but how do we deal with integer overflow? Originally incrementBy was always 1 so it's unlikely to happen, but with incrementBy being configurable now, it's technically more likely now if the user sets a relatively large value.

Can we check how v1's mapper handles this (if at all)?

Comment on lines +160 to +164
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))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. Why do we need to check existingVersionValue.get().n() != null && ((versionStartAtFromAnnotation.isPresent() && Long.parseLong(existingVersionValue.get().n()) == versionStartAtFromAnnotation.get()) || Long.parseLong(existingVersionValue.get().n()) == this.startAt)))?

Suggesting the following

if (existingValueNotAvailable(existingAttributeValue)) {
    long startVersion = versionStartAtFromAnnotation.orElse(this.startAt);
    long incrementVersion = versionIncrementByFromAnnotation.orElse(this.incrementBy);
  ...
} else {
  ....
}

@@ -144,11 +202,38 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex

@NotThreadSafe
public static final class Builder {
private Long startAt = 0L;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason we are setting the default values in Builder? Builder class should not have any logic ideally and it also seems we are already setting the default values in the top level classes

.operationContext(PRIMARY_CONTEXT)
.tableMetadata(FakeItem.getTableMetadata())
.build());
}

@Test
public void beforeWrite_versionEqualsStartAt_treatedAsInitialVersion() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add tests for different table schemas such as static table schema, immutable table scheme and bean table schema? https://github.com/aws/aws-sdk-java-v2/pull/6067/files#diff-9826429ca566bd1325f9ffa5d4fe1141cd26df01ec67262bab6f4acb12c1aa28

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants