Support update expressions in single request update#6471
Support update expressions in single request update#6471anasatirbasa wants to merge 5 commits intoaws:masterfrom
Conversation
bfa9f26 to
a7fe576
Compare
f26a4ec to
8791c1f
Compare
ef8f4c0 to
a0eeea7
Compare
716b7c8 to
dabb912
Compare
| .ignoreNulls(ignoreNulls) | ||
| .ignoreNullsMode(ignoreNullsMode) | ||
| .conditionExpression(conditionExpression) | ||
| .returnValuesOnConditionCheckFailure(returnValuesOnConditionCheckFailure); |
There was a problem hiding this comment.
Should we add updateExpression here?
There was a problem hiding this comment.
Yes, it should be added to the buillder. I've made the changes.
| * request expressions (highest). | ||
| * | ||
| * <p><b>Steps:</b> Identify attributes used by extensions/requests to prevent REMOVE conflicts → | ||
| * create item SET/REMOVE actions → merge extensions (override item) → merge request (override all). |
There was a problem hiding this comment.
This is not happening, this concatenates and DynamoDB will throw a conflict error, see updateExpressionInRequest_whenAttributeAlsoInPojo_shouldThrowConflictError test.
| .collect(Collectors.toSet()); | ||
| } | ||
|
|
||
| public static UpdateExpression generateItemSetExpression(Map<String, AttributeValue> itemMap, |
There was a problem hiding this comment.
Should this be package private as it is used only by unit tests?
Same for generateItemRemoveExpression.
There was a problem hiding this comment.
I have converted them into package private methods.
| .build(); | ||
| } | ||
|
|
||
| public static UpdateExpression generateItemRemoveExpression(Map<String, AttributeValue> itemMap, |
There was a problem hiding this comment.
I have updated it to private.
| .collect(Collectors.toSet()); | ||
| } | ||
|
|
||
| public static UpdateExpression generateItemSetExpression(Map<String, AttributeValue> itemMap, |
There was a problem hiding this comment.
I have updated it to private.
| } | ||
|
|
||
| public UpdateExpressionResolver build() { | ||
| return new UpdateExpressionResolver(this); |
There was a problem hiding this comment.
If nonKeyAttributes() is never called on the builder, this.nonKeyAttributes remains null. The resolve() method calls nonKeyAttributes.isEmpty() which will throw NullPointerException.
| } | ||
|
|
||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
We don't have new line at the end
| * if there are attributes to be updated (most likely). If both exist, they are merged and the code generates a final | ||
| * Expression that represent the result. | ||
| * Merges UpdateExpressions from three sources in priority order: POJO attributes (lowest), | ||
| * extensions (medium), request (highest). Higher priority sources override conflicting actions. |
There was a problem hiding this comment.
We may want to clarify the javadoc here, If I understand this right,
Given a POJO with: item.setCounter(100L)
And a request UpdateExpression: SET counter = counter + :inc
The resolver produces BOTH actions (concat, not override):
SET #AMZN_MAPPED_counter = :AMZN_MAPPED_counter, counter = counter + :inc
DynamoDB rejects this with: "Two document paths overlap with each other"
The only conflict that IS auto-resolved: if item.setCounter(null), the POJO would generate REMOVE counter, but the resolver suppresses that REMOVE because counter appears in the request expression.
| * Tests transactWriteItems() operation Verifies that transactional write operations work correctly. | ||
| */ | ||
| @Test | ||
| public void transactWriteItems_withUpdateExpression() { |
There was a problem hiding this comment.
This test does NOT exercise TransactUpdateItemEnhancedRequest.updateExpression(), it only performs transactional delete + put operations. There is no test that verifies the request-level
UpdateExpression flows through the TransactUpdateItemEnhancedRequest → UpdateItemOperation →
UpdateExpressionResolver path (the right-side of the Either).
A test using:
.addUpdateItem(mappedTable, TransactUpdateItemEnhancedRequest.builder(...)
.item(keyRecord).updateExpression(expression).build())is needed to cover this code path.
|
The available approaches were analyzed and the main goal was to avoid breaking backward compatibility. If this is not set, behavior stays LEGACY (same as today). The one existing exception is null-remove suppression: if a POJO field is null, it would normally produce a REMOVE, but that REMOVE is skipped when the same attribute is explicitly updated by an extension or request expression. This existing protective behavior is unchanged. If users want conflict handling, they can opt into PRIORITIZE_HIGHER_SOURCE. Examples:
Tha Javadoc was also updated to reflect all of this: why the flag exists, default behavior, available values, precedence, and path grouping. Tests were updated and expanded to cover these scenarios end-to-end, including:
|
Motivation and Context
#5554
This enhancement adds support for explicit
UpdateExpressioninput in the DynamoDB Enhanced Client while preserving existing behavior by default.Previously, users could update items using POJO attributes and extension-generated expressions, but could not directly provide request-level update expressions. That made it harder to use advanced DynamoDB update patterns (for example, atomic counters, list operations, and custom expression logic) in the same request flow.
To keep this change safe for existing users, we introduced an opt-in merge strategy flag instead of changing merge behavior globally.
What Changed
1) Enhanced Request Models
updateExpression()support to:UpdateItemEnhancedRequestTransactUpdateItemEnhancedRequestupdateExpressionMergeStrategy()to both request builders.2) Backward Compatibility Preserved by Default
UpdateExpressionMergeStrategy.LEGACY.Two document paths overlap)3) New Opt-In Merge Strategy
UpdateExpressionMergeStrategy.PRIORITIZE_HIGHER_SOURCE.Top-level examples:
list,list[0],list[1]-> same top-level attribute (list)object.list[0]-> top-level attribute (object)So
list,list[0], andlist[1]compete in the same group, whileobject.list[0]is resolved in a different group.4) Existing Null-Remove Behavior Kept
REMOVE attrREMOVEis suppressedTesting
Coverage Added/Updated
Tests were updated and expanded to cover:
Legacy default behavior
Opt-in strategy behavior
PRIORITIZE_HIGHER_SOURCEprecedence resolution across sourcesPath grouping scenarios
list,list[0],list[1]object.list[0]Conflict and suppression scenarios
Non-overlapping attributes
Test Results
Test Coverage on modified classes:
Test Coverage Checklist
Types of changes
Checklist
mvn installsucceedsscripts/new-changescript and following the instructions. Commit the new file created by the script in.changes/next-releasewith your changes.License