Skip to content

Conversation

@RanVaknin
Copy link
Contributor

@RanVaknin RanVaknin commented Dec 22, 2025

Notes

This PR is part of a two part approach on a feature branch. The current PR focuses specifically on unblocking existing SDK v1 records by resolving the ambiguity for update operations, while a followup PR will address the remaining gaps to allow NEW records to be created with explicit version=0 values.

Context And Problem

In July 2024, PR #6019 introduced custom startAt and incrementBy functionality to the VersionedRecordExtension, allowing users to configure initial version values and increment amounts. However, this inadvertently created a bug in the isInitialVersion() method that incorrectly identifies existing records with version = startAt as initial versions, causing them to be treated with attribute_not_exists.

This bug became a significant blocker for SDK v1 to v2 migrations. In SDK v1, the SDK allows the option to start at version 0, meaning some existing production records might have version = 0. When migrating to SDK v2 with the default startAt = 0 configuration, these existing records are incorrectly identified as initial versions, causing update operations to fail with condition check exceptions.

The problem:

private boolean isInitialVersion(AttributeValue existingVersionValue, Long versionStartAtFromAnnotation) {
    if (existingVersionValue == null || isNullAttributeValue(existingVersionValue)) {
        return true;  // Unambiguous: definitely a new record
    }
    
    if (existingVersionValue.n() != null) {
        long currentVersion = Long.parseLong(existingVersionValue.n());
        Long effectiveStartAt = versionStartAtFromAnnotation != null ? versionStartAtFromAnnotation : this.startAt;
        return currentVersion == effectiveStartAt;  // Ambiguous: existing record? or user setting .setVersion() on the record manually
    }
    
    return false;
}

This is caused by an ambiguity problem where the client side code cannot distinguish between a new record where the user explicitly sets version = startAt vs an existing record that happens to have version = startAt. The isInitialVersion() method made incorrect assumptions by treating both cases identically, leading to the wrong DynamoDB condition expressions being generated.

The fix:

We resolve the ambiguity by pushing the disambiguation logic to DynamoDB using an OR condition.

Key Changes:

Handle the ambiguous case with OR condition:

// version is null: must be a new record
if (existingVersionValue == null || isNullAttributeValue(existingVersionValue)) {
    newVersionValue = AttributeValue.builder()
                                    .n(Long.toString(versionStartAtFromAnnotation + versionIncrementByFromAnnotation))
                                    .build();
    condition = Expression.builder()
                          .expression(String.format("attribute_not_exists(%s)", attributeKeyRef))
                          .expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeKey.get()))
                          .build();                          
else {
    long existingVersion = Long.parseLong(existingVersionValue.n());
    newVersionValue = AttributeValue.builder().n(Long.toString(existingVersion + increment)).build();
    
    if (existingVersion == versionStartAtFromAnnotation) {
        // Ambiguous case: use OR condition
        condition = Expression.builder()
                              .expression(String.format("attribute_not_exists(%s) OR %s = %s",
                                                      attributeKeyRef, attributeKeyRef, existingVersionValueKey))
                              .expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeKey.get()))
                              .expressionValues(Collections.singletonMap(existingVersionValueKey, existingVersionValue))
                              .build();
    } else {
        // Unambiguous: must be existing record
        condition = Expression.builder()
                              .expression(String.format("%s = %s", attributeKeyRef, existingVersionValueKey))
                              .expressionNames(Collections.singletonMap(attributeKeyRef, versionAttributeKey.get()))
                              .expressionValues(Collections.singletonMap(existingVersionValueKey, existingVersionValue))
                              .build();
    }
}

This way we can account for the 3 possible scenarios:

  1. version is null - definitely a new record.
  2. existing version is different than the supplied version (definitely existing record)
  3. existing version is EQUAL to the supplied version - ambiguous. Could be user setting .setVersion(0) on a new record or could be a record we just read from the table that has a version = 0.

By using the OR operator in the DDB expression, we allow the DDB server to disambiguate this for us.

@RanVaknin RanVaknin requested a review from a team as a code owner December 22, 2025 23:21
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.

2 participants