This is an automated email from the ASF dual-hosted git repository.

virajjasani pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/phoenix-adapters.git


The following commit(s) were added to refs/heads/main by this push:
     new 1519754  Support ReturnValues UPDATED_NEW and UPDATED_OLD for 
UpdateItem API
1519754 is described below

commit 1519754ffc36f37c9a7f115768dfc576b0e29291
Author: Viraj Jasani <[email protected]>
AuthorDate: Thu Jun 25 10:38:04 2026 -0700

    Support ReturnValues UPDATED_NEW and UPDATED_OLD for UpdateItem API
---
 DDB_API_REFERENCE.md                               |   47 +-
 .../phoenix/ddb/service/UpdateItemService.java     |   20 +-
 .../apache/phoenix/ddb/service/utils/DMLUtils.java |   60 +-
 .../phoenix/ddb/service/utils/ValidationUtil.java  |   17 +-
 .../apache/phoenix/ddb/CommonServiceUtilsTest.java |   92 ++
 .../apache/phoenix/ddb/UpdateItemBaseTests.java    |    2 +
 .../java/org/apache/phoenix/ddb/UpdateItemIT.java  | 1395 +++++++++++++++++++-
 .../java/org/apache/phoenix/ddb/ValidationIT.java  |   25 +-
 .../apache/phoenix/ddb/bson/BsonDocumentToMap.java |   42 +-
 .../phoenix/ddb/utils/CommonServiceUtils.java      |   21 +
 10 files changed, 1652 insertions(+), 69 deletions(-)

diff --git a/DDB_API_REFERENCE.md b/DDB_API_REFERENCE.md
index cd22828..9495e8b 100644
--- a/DDB_API_REFERENCE.md
+++ b/DDB_API_REFERENCE.md
@@ -285,8 +285,47 @@ Controls what data is returned after a write operation.
 | `NONE` | Yes (default) | Yes (default) | Yes (default) | Returns nothing 
(only `ConsumedCapacity`) |
 | `ALL_OLD` | Yes | Yes | Yes | Returns the item as it was **before** the 
operation |
 | `ALL_NEW` | No | Yes | No | Returns the item as it is **after** the 
operation |
-| `UPDATED_OLD` | -- | **Not supported** (throws 400), use `ALL_OLD` instead | 
-- | Only applicable to UpdateItem |
-| `UPDATED_NEW` | -- | **Not supported** (throws 400), use `ALL_NEW` instead | 
-- | Only applicable to UpdateItem |
+| `UPDATED_OLD` | No (throws 400) | Yes | No (throws 400) | Returns **only the 
touched attribute paths**, with their **before-update** values |
+| `UPDATED_NEW` | No (throws 400) | Yes | No (throws 400) | Returns **only the 
touched attribute paths**, with their **after-update** values |
+
+**`UPDATED_OLD` / `UPDATED_NEW` projection semantics (UpdateItem only)**
+
+The projection covers the union of attribute paths referenced by the 
`UpdateExpression`'s `SET`,
+`REMOVE`, `ADD`, and `DELETE` clauses (or the corresponding `AttributeUpdates` 
entries).
+
+**Core rule.** A touched attribute path `P` contributes the **entire top-level 
attribute**
+rooted at `P`'s first segment to the response if and only if `P` resolves to 
an existing element
+in the relevant image (OLD image for `UPDATED_OLD`, post-update NEW image for 
`UPDATED_NEW`).
+Multiple touched paths sharing the same top-level attribute de-duplicate to a 
single copy.
+This matches AWS's documented behavior: "If you update a portion of a nested 
attribute, the
+response includes the entire top-level attribute."
+
+Consequences of the core rule, by clause:
+
+- **Top-level `SET` / `ADD`** (e.g. `SET COL2 = :v`, `ADD Counter :n`): the 
path always
+  resolves in NEW (it was just written/incremented); in OLD it resolves iff 
the attribute
+  pre-existed. Response is `{"COL2": <value>}` / `{"Counter": <value>}` when 
the path resolves,
+  empty otherwise (e.g. `ADD` on a previously-missing attribute is absent from 
`UPDATED_OLD`).
+- **Nested `SET`** (e.g. `SET nested.field = :v`, `SET items[0].sku = :v`): 
the leaf always
+  resolves in NEW (the SET created/overwrote it); in OLD it resolves iff that 
exact leaf
+  pre-existed. The whole top-level (`nested` / `items`) is emitted — untouched 
siblings inside
+  the map and untouched indices inside the list are preserved verbatim.
+- **Top-level `REMOVE`** (e.g. `REMOVE COL2`): the path is gone in NEW, so 
`UPDATED_NEW` omits
+  it (`{}`); it existed in OLD, so `UPDATED_OLD` returns the pre-removal value.
+- **Nested map-field `REMOVE`** (e.g. `REMOVE myMap.field`): `myMap.field` is 
gone in NEW so
+  `UPDATED_NEW` is `{}` even though `myMap` itself still exists; `UPDATED_OLD` 
returns the
+  whole pre-removal `myMap` (including the removed field).
+- **Nested list-index `REMOVE`** (e.g. `REMOVE myList[N]`): DDB shortens the 
list by one. The
+  leaf `myList[N]` resolves in NEW iff `N < newLength`. So removing an 
interior index
+  (`REMOVE myList[2]` on a 5-element list) leaves the post-shift element at 
`[2]` and emits
+  the whole post-image list; removing the last index (`REMOVE myList[4]` on a 
5-element list)
+  leaves `[4]` past the new length so `UPDATED_NEW` is `{}`. `UPDATED_OLD` 
always emits the
+  whole pre-removal list (the index always existed pre-removal).
+- **`DELETE`** (set-element removal, e.g. `DELETE TopLevelSet :v`): the 
touched attribute is
+  the top-level set itself, which always still exists post-delete (DDB rejects 
empty sets).
+  `UPDATED_OLD` returns the pre-delete set; `UPDATED_NEW` returns the 
post-delete set.
+- **Primary-key columns** are never included unless they are themselves 
touched by the update
+  expression, which is impossible for keys.
 
 ### 5.6 ReturnValuesOnConditionCheckFailure
 
@@ -953,7 +992,7 @@ Supported `SET` functions and operators:
 
 - `UpdateExpression` and `AttributeUpdates` are mutually exclusive (throws 
400; use one or the other)
 - `ConditionExpression` and `Expected` are mutually exclusive (throws 400; use 
one or the other)
-- `ReturnValues` must be `NONE`, `ALL_OLD`, or `ALL_NEW` (`UPDATED_OLD` and 
`UPDATED_NEW` throw 400; use `ALL_OLD` or `ALL_NEW` instead)
+- `ReturnValues` must be `NONE`, `ALL_OLD`, `ALL_NEW`, `UPDATED_OLD`, or 
`UPDATED_NEW`
 - Invalid update expression paths throw 400 with `ValidationException("Invalid 
document path used for update")`
 
 ---
@@ -1806,8 +1845,6 @@ Each API operation tracks:
 
 | Feature | Status                                             |
 |---|----------------------------------------------------|
-| `UPDATED_OLD` return value (UpdateItem) | Not supported (throws 400), use 
`ALL_OLD` instead. |
-| `UPDATED_NEW` return value (UpdateItem) | Not supported (throws 400), use 
`ALL_NEW` instead. |
 | Disabling streams | Not supported once enabled                         |
 | Continuous Backups / PITR | Stub only (always returns DISABLED)              
  |
 | Transactions (`TransactWriteItems`, `TransactGetItems`) | Not implemented    
                                |
diff --git 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/UpdateItemService.java
 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/UpdateItemService.java
index cdcd9b7..e487345 100644
--- 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/UpdateItemService.java
+++ 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/UpdateItemService.java
@@ -6,13 +6,13 @@ import java.sql.SQLException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.phoenix.ddb.service.exceptions.ValidationException;
 import 
org.apache.phoenix.expression.util.bson.BsonUpdateInvalidArgumentException;
 import org.apache.phoenix.expression.util.bson.UpdateExpressionUtils;
 import org.bson.BsonDocument;
 import org.bson.BsonValue;
-import org.bson.RawBsonDocument;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -85,10 +85,24 @@ public class UpdateItemService {
             boolean hasCondExp = 
(request.get(ApiMetadata.CONDITION_EXPRESSION) != null) || (
                     request.get(ApiMetadata.EXPECTED) != null);
 
+            String returnValue = (String) 
request.get(ApiMetadata.RETURN_VALUES);
+            Set<String> touchedPaths = null;
+            if (ApiMetadata.UPDATED_OLD.equals(returnValue) || 
ApiMetadata.UPDATED_NEW.equals(
+                returnValue)) {
+                touchedPaths =
+                    
CommonServiceUtils.getTouchedPathsFromUpdateDoc(statementInfo.updateDoc);
+                if (touchedPaths.isEmpty()) {
+                    throw new PhoenixServiceException(
+                        "UpdateItem with UPDATED_OLD/UPDATED_NEW requires a 
non-empty "
+                            + "UpdateExpression or AttributeUpdates");
+                }
+            }
+
             Map<String, Object> res = 
DMLUtils.executeUpdate(statementInfo.stmt,
-                    (String) request.get(ApiMetadata.RETURN_VALUES),
+                    returnValue,
                     (String) 
request.get(ApiMetadata.RETURN_VALUES_ON_CONDITION_CHECK_FAILURE),
-                    hasCondExp, statementInfo.canEvaluateUpdateExprOnEmptyDoc, 
pkCols, ApiOperation.UPDATE_ITEM);
+                    hasCondExp, statementInfo.canEvaluateUpdateExprOnEmptyDoc, 
pkCols,
+                    ApiOperation.UPDATE_ITEM, touchedPaths);
             res.put(ApiMetadata.CONSUMED_CAPACITY,
                     
CommonServiceUtils.getConsumedCapacity((String)request.get(ApiMetadata.TABLE_NAME)));
             return res;
diff --git 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/DMLUtils.java
 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/DMLUtils.java
index 57a8934..2aa8624 100644
--- 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/DMLUtils.java
+++ 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/DMLUtils.java
@@ -7,6 +7,8 @@ import java.util.Base64;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 
 import org.apache.phoenix.ddb.service.exceptions.ValidationException;
 import org.bson.RawBsonDocument;
@@ -53,19 +55,18 @@ public class DMLUtils {
         }
     }
 
-    /**
-     * Executes the given PreparedStatement of an UPSERT query for 
PutItem/UpdateItem API.
-     *
-     * If conditionExpression is given and it fails, throw 
ConditionalCheckFailedException.
-     *  - if returnValuesOnConditionCheckFailure is ALL_OLD, set the item on 
the Exception
-     * If conditionExpression succeeds return the item with type of 
returnValue.
-     *
-     * TODO: UPDATED_OLD | UPDATED_NEW
-     */
     public static Map<String, Object> executeUpdate(PreparedStatement stmt, 
String returnValue,
-        String returnValuesOnConditionCheckFailure, boolean hasCondExp, 
boolean canEvaluateExprOnEmptyDoc,
-                                                    List<PColumn> pkCols, 
ApiOperation apiOperation)
-                            throws SQLException, ConditionCheckFailedException 
{
+        String returnValuesOnConditionCheckFailure, boolean hasCondExp,
+        boolean canEvaluateExprOnEmptyDoc, List<PColumn> pkCols, ApiOperation 
apiOperation)
+        throws SQLException, ConditionCheckFailedException {
+        return executeUpdate(stmt, returnValue, 
returnValuesOnConditionCheckFailure, hasCondExp,
+            canEvaluateExprOnEmptyDoc, pkCols, apiOperation, null);
+    }
+
+    public static Map<String, Object> executeUpdate(PreparedStatement stmt, 
String returnValue,
+        String returnValuesOnConditionCheckFailure, boolean hasCondExp,
+        boolean canEvaluateExprOnEmptyDoc, List<PColumn> pkCols, ApiOperation 
apiOperation,
+        Set<String> updatedAttributePaths) throws SQLException, 
ConditionCheckFailedException {
         try {
             Map<String, Object> returnAttrs = new HashMap<>();
             if (!needReturnRow(returnValue, 
returnValuesOnConditionCheckFailure)) {
@@ -92,8 +93,7 @@ public class DMLUtils {
                 return new HashMap<>();
             }
             Pair<Integer, ResultSet> resultPair;
-            if (ApiMetadata.ALL_OLD.equals(returnValue)
-                && apiOperation != ApiOperation.DELETE_ITEM) {
+            if (returnOldImage(returnValue) && apiOperation != 
ApiOperation.DELETE_ITEM) {
                 resultPair =
                     
stmt.unwrap(PhoenixPreparedStatement.class).executeAtomicUpdateReturnOldRow();
             } else {
@@ -126,19 +126,28 @@ public class DMLUtils {
             } else {
                 boolean returnValuesInResponse = false;
                 if (apiOperation != ApiOperation.DELETE_ITEM) {
-                    // TODO : reject UPDATED_OLD, UPDATED_NEW cases which are 
not supported
-                    if (ApiMetadata.ALL_NEW.equals(returnValue) || 
ApiMetadata.ALL_OLD.equals(
-                        returnValue)) {
+                    if (returnValueInResp(returnValue)) {
                         returnValuesInResponse = true;
                     }
                 } else if (ApiMetadata.ALL_OLD.equals(returnValue) && 
rawBsonDocument != null) {
                     returnValuesInResponse = true;
                 }
                 if (returnValuesInResponse) {
-                    returnAttrs = 
BsonDocumentToMap.getFullItem(rawBsonDocument);
-                    Map<String, Object> tmpReturnAttrs = returnAttrs;
+                    boolean projectTouchedPaths = 
ApiMetadata.UPDATED_OLD.equals(returnValue)
+                        || ApiMetadata.UPDATED_NEW.equals(returnValue);
+                    Map<String, Object> attrMap;
+                    if (projectTouchedPaths) {
+                        Objects.requireNonNull(updatedAttributePaths,
+                            "UPDATED_OLD/UPDATED_NEW requires a non-null 
touched-path set");
+                        attrMap = rawBsonDocument == null ?
+                            new HashMap<>() :
+                            BsonDocumentToMap.getProjectedItem(rawBsonDocument,
+                                updatedAttributePaths, true);
+                    } else {
+                        attrMap = 
BsonDocumentToMap.getFullItem(rawBsonDocument);
+                    }
                     returnAttrs = new HashMap<>();
-                    returnAttrs.put(ApiMetadata.ATTRIBUTES, tmpReturnAttrs);
+                    returnAttrs.put(ApiMetadata.ATTRIBUTES, attrMap);
                 }
             }
             return returnAttrs;
@@ -151,6 +160,17 @@ public class DMLUtils {
         }
     }
 
+    private static boolean returnValueInResp(String returnValue) {
+        return ApiMetadata.ALL_NEW.equals(returnValue) || 
ApiMetadata.ALL_OLD.equals(returnValue)
+            || ApiMetadata.UPDATED_OLD.equals(returnValue) || 
ApiMetadata.UPDATED_NEW.equals(
+            returnValue);
+    }
+
+    private static boolean returnOldImage(String returnValue) {
+        return ApiMetadata.ALL_OLD.equals(returnValue) || 
ApiMetadata.UPDATED_OLD.equals(
+            returnValue);
+    }
+
     /**
      * Use return row api only if
      * returnValue is not empty/null and not NONE
diff --git 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/ValidationUtil.java
 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/ValidationUtil.java
index faba22d..e589f4a 100644
--- 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/ValidationUtil.java
+++ 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/ValidationUtil.java
@@ -163,7 +163,7 @@ public class ValidationUtil {
      * Validates the ReturnValues parameter based on DynamoDB API 
specifications.
      * For PutItem: Only NONE and ALL_OLD are valid
      * For DeleteItem: Only NONE and ALL_OLD are valid
-     * For UpdateItem: All values are valid (NONE, ALL_OLD, ALL_NEW, 
UPDATED_OLD, UPDATED_NEW)
+     * For UpdateItem: NONE, ALL_OLD, ALL_NEW, UPDATED_OLD, UPDATED_NEW are 
valid
      *
      * @throws ValidationException
      */
@@ -184,13 +184,13 @@ public class ValidationUtil {
                 break;
 
             case UPDATE_ITEM:
-                // All values are valid for UpdateItem
-                if (!ApiMetadata.ALL_OLD.equals(returnValue) && 
!ApiMetadata.ALL_NEW.equals(
-                    returnValue) && 
!ApiMetadata.UPDATED_OLD.equals(returnValue)
-                    && !ApiMetadata.UPDATED_NEW.equals(returnValue)) {
+                if (!ApiMetadata.ALL_OLD.equals(returnValue)
+                        && !ApiMetadata.ALL_NEW.equals(returnValue)
+                        && !ApiMetadata.UPDATED_OLD.equals(returnValue)
+                        && !ApiMetadata.UPDATED_NEW.equals(returnValue)) {
                     throw new ValidationException(String.format(
                         "ReturnValues value '%s' is not valid for UpdateItem. 
Valid "
-                            + "values are: NONE, ALL_OLD, ALL_NEW",
+                            + "values are: NONE, ALL_OLD, ALL_NEW, 
UPDATED_OLD, UPDATED_NEW",
                         returnValue));
                 }
                 break;
@@ -219,8 +219,9 @@ public class ValidationUtil {
 
     public static void validateReturnValuesRequest(String returnValue,
         String returnValuesOnConditionCheckFailure, ApiOperation apiOperation) 
{
-        if (ApiMetadata.UPDATED_OLD.equals(returnValue) || 
ApiMetadata.UPDATED_NEW.equals(
-            returnValue)) {
+        if ((ApiMetadata.UPDATED_OLD.equals(returnValue)
+                || ApiMetadata.UPDATED_NEW.equals(returnValue))
+                && apiOperation != ApiOperation.UPDATE_ITEM) {
             throw new ValidationException(
                 "UPDATED_OLD or UPDATED_NEW is not supported for 
ReturnValue.");
         }
diff --git 
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/CommonServiceUtilsTest.java
 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/CommonServiceUtilsTest.java
index 963d32a..4b771b1 100644
--- 
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/CommonServiceUtilsTest.java
+++ 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/CommonServiceUtilsTest.java
@@ -19,8 +19,13 @@ package org.apache.phoenix.ddb;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.phoenix.ddb.utils.CommonServiceUtils;
+import org.bson.BsonDocument;
+import org.bson.BsonInt32;
+import org.bson.BsonNull;
+import org.bson.BsonString;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -253,4 +258,91 @@ public class CommonServiceUtilsTest {
         String result = 
CommonServiceUtils.replaceExpressionAttributeNames(input, exprAttrNames);
         Assert.assertEquals("ab", result);
     }
+
+    // ---- getTouchedPathsFromUpdateDoc ----
+
+    @Test
+    public void testGetTouchedPathsFromUpdateDoc_null() {
+        Set<String> paths = 
CommonServiceUtils.getTouchedPathsFromUpdateDoc(null);
+        Assert.assertNotNull(paths);
+        Assert.assertTrue(paths.isEmpty());
+    }
+
+    @Test
+    public void testGetTouchedPathsFromUpdateDoc_emptyDoc() {
+        Set<String> paths = 
CommonServiceUtils.getTouchedPathsFromUpdateDoc(new BsonDocument());
+        Assert.assertTrue(paths.isEmpty());
+    }
+
+    @Test
+    public void testGetTouchedPathsFromUpdateDoc_setOnly() {
+        BsonDocument updateDoc = new BsonDocument();
+        BsonDocument setDoc = new BsonDocument();
+        setDoc.put("name", new BsonString("Bob"));
+        setDoc.put("address.city", new BsonString("Portland"));
+        updateDoc.put("$SET", setDoc);
+
+        Set<String> paths = 
CommonServiceUtils.getTouchedPathsFromUpdateDoc(updateDoc);
+        Assert.assertEquals(2, paths.size());
+        Assert.assertTrue(paths.contains("name"));
+        Assert.assertTrue(paths.contains("address.city"));
+    }
+
+    @Test
+    public void testGetTouchedPathsFromUpdateDoc_allClauses() {
+        BsonDocument updateDoc = new BsonDocument();
+        BsonDocument setDoc = new BsonDocument();
+        setDoc.put("name", new BsonString("Bob"));
+        updateDoc.put("$SET", setDoc);
+
+        BsonDocument unsetDoc = new BsonDocument();
+        unsetDoc.put("oldField", BsonNull.VALUE);
+        unsetDoc.put("nested.legacy", BsonNull.VALUE);
+        updateDoc.put("$UNSET", unsetDoc);
+
+        BsonDocument addDoc = new BsonDocument();
+        addDoc.put("counter", new BsonInt32(1));
+        updateDoc.put("$ADD", addDoc);
+
+        BsonDocument delDoc = new BsonDocument();
+        delDoc.put("tags", new BsonString(":t1"));
+        updateDoc.put("$DELETE_FROM_SET", delDoc);
+
+        Set<String> paths = 
CommonServiceUtils.getTouchedPathsFromUpdateDoc(updateDoc);
+        Assert.assertEquals(5, paths.size());
+        Assert.assertTrue(paths.contains("name"));
+        Assert.assertTrue(paths.contains("oldField"));
+        Assert.assertTrue(paths.contains("nested.legacy"));
+        Assert.assertTrue(paths.contains("counter"));
+        Assert.assertTrue(paths.contains("tags"));
+    }
+
+    @Test
+    public void testGetTouchedPathsFromUpdateDoc_listIndexPath() {
+        BsonDocument updateDoc = new BsonDocument();
+        BsonDocument setDoc = new BsonDocument();
+        // Path produced by PathResolver for `SET items[0].sku = :v` is 
"items[0].sku".
+        setDoc.put("items[0].sku", new BsonString("ABC"));
+        updateDoc.put("$SET", setDoc);
+
+        Set<String> paths = 
CommonServiceUtils.getTouchedPathsFromUpdateDoc(updateDoc);
+        Assert.assertEquals(1, paths.size());
+        Assert.assertTrue(paths.contains("items[0].sku"));
+    }
+
+    @Test
+    public void 
testGetTouchedPathsFromUpdateDoc_unknownTopLevelKeysAreIgnored() {
+        // The adapter never emits these, but extra keys outside the four 
supported clauses
+        // must not pollute the touched-path set.
+        BsonDocument updateDoc = new BsonDocument();
+        BsonDocument setDoc = new BsonDocument();
+        setDoc.put("name", new BsonString("Bob"));
+        updateDoc.put("$SET", setDoc);
+        updateDoc.put("$WHATEVER",
+                new BsonDocument("ignored", new BsonString("x")));
+
+        Set<String> paths = 
CommonServiceUtils.getTouchedPathsFromUpdateDoc(updateDoc);
+        Assert.assertEquals(1, paths.size());
+        Assert.assertTrue(paths.contains("name"));
+    }
 }
diff --git 
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/UpdateItemBaseTests.java
 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/UpdateItemBaseTests.java
index a131d28..aa085ed 100644
--- 
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/UpdateItemBaseTests.java
+++ 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/UpdateItemBaseTests.java
@@ -57,6 +57,7 @@ import org.apache.hadoop.hbase.HBaseTestingUtility;
 import org.apache.phoenix.ddb.rest.RESTServer;
 import org.apache.phoenix.end2end.ServerMetadataCacheTestImpl;
 import org.apache.phoenix.jdbc.PhoenixDriver;
+import org.apache.phoenix.jdbc.PhoenixTestDriver;
 import org.apache.phoenix.util.PhoenixRuntime;
 import org.apache.phoenix.util.ServerUtil;
 
@@ -102,6 +103,7 @@ public class UpdateItemBaseTests {
         utility.startMiniCluster();
         String zkQuorum = "localhost:" + 
utility.getZkCluster().getClientPort();
         url = PhoenixRuntime.JDBC_PROTOCOL + 
PhoenixRuntime.JDBC_PROTOCOL_SEPARATOR + zkQuorum;
+        DriverManager.registerDriver(new PhoenixTestDriver());
 
         restServer = new RESTServer(utility.getConfiguration());
         restServer.run();
diff --git 
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/UpdateItemIT.java 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/UpdateItemIT.java
index 9e1d71b..f3438e5 100644
--- a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/UpdateItemIT.java
+++ b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/UpdateItemIT.java
@@ -534,35 +534,25 @@ public class UpdateItemIT extends UpdateItemBaseTests {
         UpdateItemResponse phoenixResult3 = 
phoenixDBClientV2.updateItem(updateRequest);
         Assert.assertEquals(dynamoResult3.attributes(), 
phoenixResult3.attributes());
 
-        // Test UPDATED_OLD - should fail with error status code in 
phoenixDBClientV2
+        // Test UPDATED_OLD - now supported on UpdateItem; returns only the 
touched attribute
+        // paths (here: just `stringField`) with their pre-update values.
         updateRequest = 
UpdateItemRequest.builder().tableName(tableName).key(key)
             .updateExpression("SET stringField = :val")
             .expressionAttributeValues(expressionAttributeValues)
             .returnValues(ReturnValue.UPDATED_OLD).build();
+        UpdateItemResponse dynamoResult4 = 
dynamoDbClient.updateItem(updateRequest);
+        UpdateItemResponse phoenixResult4 = 
phoenixDBClientV2.updateItem(updateRequest);
+        Assert.assertEquals(dynamoResult4.attributes(), 
phoenixResult4.attributes());
 
-        try {
-            phoenixDBClientV2.updateItem(updateRequest);
-            Assert.fail("Expected DynamoDbException for UPDATED_OLD");
-        } catch (DynamoDbException e) {
-            Assert.assertEquals(400, e.statusCode());
-            Assert.assertTrue(e.getMessage()
-                .contains("UPDATED_OLD or UPDATED_NEW is not supported for 
ReturnValue"));
-        }
-
-        // Test UPDATED_NEW - should fail with error status code in 
phoenixDBClientV2
+        // Test UPDATED_NEW - now supported on UpdateItem; returns only the 
touched attribute
+        // paths with their post-update values.
         updateRequest = 
UpdateItemRequest.builder().tableName(tableName).key(key)
             .updateExpression("SET stringField = :val")
             .expressionAttributeValues(expressionAttributeValues)
             .returnValues(ReturnValue.UPDATED_NEW).build();
-
-        try {
-            phoenixDBClientV2.updateItem(updateRequest);
-            Assert.fail("Expected DynamoDbException for UPDATED_NEW");
-        } catch (DynamoDbException e) {
-            Assert.assertEquals(400, e.statusCode());
-            Assert.assertTrue(e.getMessage()
-                .contains("UPDATED_OLD or UPDATED_NEW is not supported for 
ReturnValue"));
-        }
+        UpdateItemResponse dynamoResult5 = 
dynamoDbClient.updateItem(updateRequest);
+        UpdateItemResponse phoenixResult5 = 
phoenixDBClientV2.updateItem(updateRequest);
+        Assert.assertEquals(dynamoResult5.attributes(), 
phoenixResult5.attributes());
 
         // Test invalid value - should fail with same error status code in 
both clients
         updateRequest = 
UpdateItemRequest.builder().tableName(tableName).key(key)
@@ -922,4 +912,1369 @@ public class UpdateItemIT extends UpdateItemBaseTests {
         phoenixDBClientV2.updateItem(req);
         validateItem(tableName, key);
     }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldTopLevelSet() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET COL2 = :v1, COL1 = :v2");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v1", AttributeValue.builder().s("NewTitle").build());
+        exprAttrVal.put(":v2", AttributeValue.builder().n("999").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewTopLevelSet() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET COL2 = :v1, COL1 = :v2");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v1", AttributeValue.builder().s("NewTitle").build());
+        exprAttrVal.put(":v2", AttributeValue.builder().n("999").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewWithRemove() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("REMOVE COL3");
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("REMOVE COL2");
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewWithAddNumber() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("ADD #c :v");
+        Map<String, String> exprAttrNames = new HashMap<>();
+        exprAttrNames.put("#c", "Counter");
+        uir1.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":v", AttributeValue.builder().n("5").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        // Re-seed so the second half starts from the same baseline (Counter 
== 66).
+        seedItem(tableName, getItem1());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("ADD #c :v");
+        uir2.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":v", AttributeValue.builder().n("4").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewWithAddOnNonExistingAttr() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("ADD FreshCounter :v");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":v", AttributeValue.builder().n("7").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("ADD AnotherFresh :v");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":v", AttributeValue.builder().n("3").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewNestedPath() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Reviews.FiveStar[0].reviewer starts at "Alice" (see getItem1()).
+        Map<String, String> exprAttrNames = new HashMap<>();
+        exprAttrNames.put("#r", "Reviews");
+        exprAttrNames.put("#f", "FiveStar");
+        exprAttrNames.put("#rv", "reviewer");
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("SET #r.#f[0].#rv = :v");
+        uir1.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":v", AttributeValue.builder().s("Carol").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("SET #r.#f[0].#rv = :v");
+        uir2.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":v", AttributeValue.builder().s("Dave").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewMixedClauses() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("SET COL2 = :s REMOVE COL3 ADD COL1 :n");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":s", AttributeValue.builder().s("UPD").build());
+        exprAttrVal1.put(":n", AttributeValue.builder().n("100").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        // Reseed so the NEW half runs against the same baseline as the OLD 
half.
+        seedItem(tableName, getItem1());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("SET COL2 = :s REMOVE COL3 ADD COL1 :n");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":s", AttributeValue.builder().s("UPD2").build());
+        exprAttrVal2.put(":n", AttributeValue.builder().n("50").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewOnItemCreation() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        // Note: createTableAndPutItem(_, false) creates the table but does 
NOT seed any row.
+        createTableAndPutItem(tableName, false);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET stringField = :s ADD #c :n");
+        Map<String, String> exprAttrNames = new HashMap<>();
+        exprAttrNames.put("#c", "counter");
+        uir.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":s", AttributeValue.builder().s("hello").build());
+        exprAttrVal.put(":n", AttributeValue.builder().n("9").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldOnItemCreation() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, false);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET stringField = :s");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":s", AttributeValue.builder().s("hello").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewConditionalSuccess() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET COL2 = :s");
+        uir.conditionExpression("COL1 = :cond");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":s", AttributeValue.builder().s("UPD").build());
+        exprAttrVal.put(":cond", AttributeValue.builder().n("1").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewConditionalFailureReturnsFullOldRow() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET COL2 = :s");
+        uir.conditionExpression("COL1 = :bogus");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":s", AttributeValue.builder().s("UPD").build());
+        exprAttrVal.put(":bogus", AttributeValue.builder().n("99999").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        uir.returnValuesOnConditionCheckFailure(ALL_OLD);
+
+        Map<String, AttributeValue> dynamoExceptionItem = null;
+        try {
+            dynamoDbClient.updateItem(uir.build());
+            Assert.fail("Expected ConditionalCheckFailedException from DDB");
+        } catch (ConditionalCheckFailedException e) {
+            Assert.assertEquals(400, e.statusCode());
+            dynamoExceptionItem = e.item();
+            Assert.assertNotNull("RVOCCF=ALL_OLD must surface the existing 
item",
+                    dynamoExceptionItem);
+        }
+        try {
+            phoenixDBClientV2.updateItem(uir.build());
+            Assert.fail("Expected ConditionalCheckFailedException from 
Phoenix");
+        } catch (ConditionalCheckFailedException e) {
+            Assert.assertEquals(400, e.statusCode());
+            Assert.assertEquals(dynamoExceptionItem, e.item());
+        }
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewWithListAppend() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Seed MyList = [a, b].
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET MyList = :init");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":init", AttributeValue.builder().l(
+                AttributeValue.builder().s("a").build(),
+                AttributeValue.builder().s("b").build()).build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("SET MyList = list_append(MyList, :extra)");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":extra", AttributeValue.builder().l(
+                AttributeValue.builder().s("c").build()).build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        // Reseed MyList = [a, b] so the NEW half starts from the same 
baseline.
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("SET MyList = list_append(MyList, :extra)");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":extra", AttributeValue.builder().l(
+                AttributeValue.builder().s("d").build(),
+                AttributeValue.builder().s("e").build()).build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewIfNotExistsOnExistingAttr() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+        // COL2 starts at "Title1" (see getItem1()).
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("SET COL2 = if_not_exists(COL2, :fallback)");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":fallback", 
AttributeValue.builder().s("Default").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("SET COL2 = if_not_exists(COL2, :fallback)");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":fallback", 
AttributeValue.builder().s("Default").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewIfNotExistsOnMissingAttr() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+        // "Subhead" is not present in getItem1().
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("SET Subhead = if_not_exists(Subhead, 
:fallback)");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":fallback", 
AttributeValue.builder().s("FirstWrite").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        // Reseed so the NEW half also runs against a row where Subhead does 
not yet exist.
+        seedItem(tableName, getItem1());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("SET Subhead = if_not_exists(Subhead, 
:fallback)");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":fallback", 
AttributeValue.builder().s("FirstWrite").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewWithArithmetic() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+        // COL1 starts at 1, COL4 starts at 34.
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("SET COL1 = COL1 + :a, COL4 = COL4 - :b");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":a", AttributeValue.builder().n("3").build());
+        exprAttrVal1.put(":b", AttributeValue.builder().n("4").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        seedItem(tableName, getItem1());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("SET COL1 = COL1 + :a, COL4 = COL4 - :b");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":a", AttributeValue.builder().n("3").build());
+        exprAttrVal2.put(":b", AttributeValue.builder().n("4").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewSiblingNestedPaths() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        Map<String, String> exprAttrNames = new HashMap<>();
+        exprAttrNames.put("#r", "Reviews");
+        exprAttrNames.put("#f", "FiveStar");
+        exprAttrNames.put("#rv", "reviewer");
+        exprAttrNames.put("#cmt", "comment");
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("SET #r.#f[0].#rv = :rv, #r.#f[0].#cmt = :cmt");
+        uir1.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":rv", AttributeValue.builder().s("Carol").build());
+        exprAttrVal1.put(":cmt", AttributeValue.builder().s("Great!").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("SET #r.#f[0].#rv = :rv, #r.#f[0].#cmt = :cmt");
+        uir2.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":rv", AttributeValue.builder().s("Dave").build());
+        exprAttrVal2.put(":cmt", 
AttributeValue.builder().s("Excellent").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewWithDeleteFromSet() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Seed a multi-element string set so a DELETE leaves the set 
non-empty.
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET TopLevelSet = :ss");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":ss", AttributeValue.builder().ss("a", "b", 
"c").build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("DELETE TopLevelSet :r");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":r", AttributeValue.builder().ss("b").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        // Reseed the set for the NEW half.
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("DELETE TopLevelSet :r");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":r", AttributeValue.builder().ss("c").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewWithAddStringSet() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+        // TopLevelSet starts as ss("setMember1") in getItem1().
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("ADD TopLevelSet :a");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":a", 
AttributeValue.builder().ss("setMember2").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        seedItem(tableName, getItem1());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("ADD TopLevelSet :a");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":a", 
AttributeValue.builder().ss("setMember3").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewAllFourClauses() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Seed TopLevelSet with multiple elements so DELETE doesn't empty it.
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET TopLevelSet = :ss");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":ss", AttributeValue.builder().ss("x", "y", 
"z").build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("SET COL2 = :s REMOVE COL3 ADD COL1 :n DELETE 
TopLevelSet :rm");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":s", AttributeValue.builder().s("UPD").build());
+        exprAttrVal1.put(":n", AttributeValue.builder().n("100").build());
+        exprAttrVal1.put(":rm", AttributeValue.builder().ss("y").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        // Reseed (including TopLevelSet) for the NEW half.
+        seedItem(tableName, getItem1());
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("SET COL2 = :s REMOVE COL3 ADD COL1 :n DELETE 
TopLevelSet :rm");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":s", AttributeValue.builder().s("UPD2").build());
+        exprAttrVal2.put(":n", AttributeValue.builder().n("50").build());
+        exprAttrVal2.put(":rm", AttributeValue.builder().ss("z").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldWhenSetCreatesNewAttribute() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        // "BrandNewField" is not present in getItem1().
+        uir1.updateExpression("SET BrandNewField = :v");
+        Map<String, AttributeValue> exprAttrVal1 = new HashMap<>();
+        exprAttrVal1.put(":v", AttributeValue.builder().s("hello").build());
+        uir1.expressionAttributeValues(exprAttrVal1);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("SET AnotherNew = :v");
+        Map<String, AttributeValue> exprAttrVal2 = new HashMap<>();
+        exprAttrVal2.put(":v", AttributeValue.builder().s("world").build());
+        uir2.expressionAttributeValues(exprAttrVal2);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldConditionalSuccess() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET COL2 = :s");
+        uir.conditionExpression("COL1 = :cond");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":s", AttributeValue.builder().s("UPD").build());
+        exprAttrVal.put(":cond", AttributeValue.builder().n("1").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewWithRvoccfAllOldOnSuccess() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET COL2 = :s");
+        uir.conditionExpression("COL1 = :cond");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":s", AttributeValue.builder().s("UPD").build());
+        exprAttrVal.put(":cond", AttributeValue.builder().n("1").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        uir.returnValuesOnConditionCheckFailure(ALL_OLD);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldWithRvoccfAllOldOnSuccess() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET COL2 = :s");
+        uir.conditionExpression("COL1 = :cond");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":s", AttributeValue.builder().s("UPD").build());
+        exprAttrVal.put(":cond", AttributeValue.builder().n("1").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_OLD);
+        uir.returnValuesOnConditionCheckFailure(ALL_OLD);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewLegacyAttributeUpdatesDelete() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Seed a multi-element string set so DELETE-with-value leaves the set 
non-empty.
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET TopLevelSet = :ss");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":ss", AttributeValue.builder().ss("a", "b", 
"c").build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        Map<String, 
software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate> updates =
+                new HashMap<>();
+        // DELETE-without-value -> REMOVE COL3
+        updates.put("COL3", 
software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate
+                .builder()
+                
.action(software.amazon.awssdk.services.dynamodb.model.AttributeAction.DELETE)
+                .build());
+        // DELETE-with-value -> DELETE_FROM_SET TopLevelSet :v
+        updates.put("TopLevelSet", 
software.amazon.awssdk.services.dynamodb.model
+                .AttributeValueUpdate.builder()
+                
.action(software.amazon.awssdk.services.dynamodb.model.AttributeAction.DELETE)
+                .value(AttributeValue.builder().ss("b").build()).build());
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.attributeUpdates(updates);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        seedItem(tableName, getItem1());
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.attributeUpdates(updates);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewWithLegacyExpected() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        Map<String, 
software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue> expected 
=
+                new HashMap<>();
+        expected.put("COL1", 
software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue
+                .builder()
+                .comparisonOperator(
+                        
software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.EQ)
+                
.attributeValueList(AttributeValue.builder().n("1").build()).build());
+
+        Map<String, 
software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate> updates =
+                new HashMap<>();
+        updates.put("COL2", 
software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate
+                .builder()
+                
.action(software.amazon.awssdk.services.dynamodb.model.AttributeAction.PUT)
+                .value(AttributeValue.builder().s("UPD").build()).build());
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.attributeUpdates(updates);
+        uir1.expected(expected);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        seedItem(tableName, getItem1());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.attributeUpdates(updates);
+        uir2.expected(expected);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testConcurrentConditionalUpdateWithUpdatedNew() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+
+        UpdateItemRequest.Builder uir =
+                UpdateItemRequest.builder().tableName(tableName).key(getKey());
+        uir.updateExpression("SET #1 = #1 + :v1");
+        uir.conditionExpression("#1 < :condVal");
+        Map<String, String> exprAttrNames = new HashMap<>();
+        exprAttrNames.put("#1", "COL1");
+        uir.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v1", AttributeValue.builder().n("10").build());
+        exprAttrVal.put(":condVal", AttributeValue.builder().n("5").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        uir.returnValuesOnConditionCheckFailure(ALL_OLD);
+
+        // Pre-condition DDB so subsequent updates fail their condition. 
Captured payload is the
+        // expected winning UPDATED_NEW projection ({COL1: {N: "11"}}).
+        Map<String, AttributeValue> newItem = 
dynamoDbClient.updateItem(uir.build()).attributes();
+
+        ExecutorService executorService = Executors.newFixedThreadPool(5);
+        AtomicInteger updateCount = new AtomicInteger(0);
+        AtomicInteger errorCount = new AtomicInteger(0);
+
+        for (int i = 0; i < 5; i++) {
+            executorService.submit(() -> {
+                Map<String, AttributeValue> oldItem = null;
+                try {
+                    dynamoDbClient.updateItem(uir.build());
+                } catch (ConditionalCheckFailedException e) {
+                    oldItem = e.item();
+                }
+                try {
+                    UpdateItemResponse result = 
phoenixDBClientV2.updateItem(uir.build());
+                    Assert.assertEquals(newItem, result.attributes());
+                    updateCount.incrementAndGet();
+                } catch (ConditionalCheckFailedException e) {
+                    Assert.assertEquals(oldItem, e.item());
+                    errorCount.incrementAndGet();
+                }
+            });
+        }
+        executorService.shutdown();
+        try {
+            boolean terminated = executorService.awaitTermination(30, 
TimeUnit.SECONDS);
+            if (terminated) {
+                Assert.assertEquals(1, updateCount.get());
+                Assert.assertEquals(4, errorCount.get());
+            } else {
+                Assert.fail(
+                        "testConcurrentConditionalUpdateWithUpdatedNew: 
threads did not terminate.");
+            }
+        } catch (InterruptedException e) {
+            Assert.fail("testConcurrentConditionalUpdateWithUpdatedNew was 
interrupted.");
+        }
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewMultiType() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        Map<String, AttributeValue> nested = new HashMap<>();
+        nested.put("inner", AttributeValue.builder().s("deep").build());
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET strAttr = :s, numAttr = :n, binAttr = :b, 
boolAttr = :bo, "
+                + "nullAttr = :nu, listAttr = :l, mapAttr = :m, ssAttr = :ss, 
nsAttr = :ns, "
+                + "bsAttr = :bs");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":s", AttributeValue.builder().s("hello").build());
+        exprAttrVal.put(":n", AttributeValue.builder().n("42").build());
+        exprAttrVal.put(":b", AttributeValue.builder()
+                .b(SdkBytes.fromByteArray(new byte[] {1, 2, 3})).build());
+        exprAttrVal.put(":bo", AttributeValue.builder().bool(true).build());
+        exprAttrVal.put(":nu", AttributeValue.builder().nul(true).build());
+        exprAttrVal.put(":l", AttributeValue.builder().l(
+                AttributeValue.builder().s("a").build(),
+                AttributeValue.builder().n("1").build()).build());
+        exprAttrVal.put(":m", AttributeValue.builder().m(nested).build());
+        exprAttrVal.put(":ss", AttributeValue.builder().ss("x", "y").build());
+        exprAttrVal.put(":ns", AttributeValue.builder().ns("1", "2", 
"3").build());
+        exprAttrVal.put(":bs", AttributeValue.builder().bs(
+                SdkBytes.fromByteArray(new byte[] {4}),
+                SdkBytes.fromByteArray(new byte[] {5})).build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewLegacyAttributeUpdates() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        Map<String, 
software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate> updates =
+                new HashMap<>();
+        updates.put("COL2", 
software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate
+                .builder()
+                
.action(software.amazon.awssdk.services.dynamodb.model.AttributeAction.PUT)
+                
.value(AttributeValue.builder().s("LegacyTitle").build()).build());
+        updates.put("Counter", 
software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate
+                .builder()
+                
.action(software.amazon.awssdk.services.dynamodb.model.AttributeAction.ADD)
+                .value(AttributeValue.builder().n("7").build()).build());
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.attributeUpdates(updates);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        seedItem(tableName, getItem1());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.attributeUpdates(updates);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewMapFieldUpdate() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Seed MyMap with three sibling fields so the multi-field UPDATE can 
leave one untouched.
+        Map<String, AttributeValue> seedMyMap = new HashMap<>();
+        seedMyMap.put("field1", AttributeValue.builder().s("orig1").build());
+        seedMyMap.put("field2", AttributeValue.builder().s("orig2").build());
+        seedMyMap.put("field3", AttributeValue.builder().s("orig3").build());
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET MyMap = :init");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":init", AttributeValue.builder().m(seedMyMap).build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET MyMap.field2 = :v1, MyMap.field3 = :v2");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v1", 
AttributeValue.builder().s("UPDATED-2").build());
+        exprAttrVal.put(":v2", 
AttributeValue.builder().s("UPDATED-3").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewDeepMapFieldUpdate() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Seed WrapperMap.middle with three siblings and WrapperMap.otherTop 
as a top-level
+        // sibling so we can touch both branches and leave one leaf untouched.
+        Map<String, AttributeValue> seedMiddle = new HashMap<>();
+        seedMiddle.put("leaf1", AttributeValue.builder().s("a").build());
+        seedMiddle.put("leaf2", AttributeValue.builder().s("b").build());
+        seedMiddle.put("leaf3", AttributeValue.builder().s("c").build());
+        Map<String, AttributeValue> seedWrapper = new HashMap<>();
+        seedWrapper.put("middle", 
AttributeValue.builder().m(seedMiddle).build());
+        seedWrapper.put("otherTop", AttributeValue.builder().s("y").build());
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET WrapperMap = :init");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":init", AttributeValue.builder().m(seedWrapper).build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression(
+            "SET WrapperMap.middle.leaf2 = :v1, WrapperMap.middle.leaf3 = :v2, 
"
+                + "WrapperMap.otherTop = :v3");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v1", 
AttributeValue.builder().s("UPDATED-leaf2").build());
+        exprAttrVal.put(":v2", 
AttributeValue.builder().s("UPDATED-leaf3").build());
+        exprAttrVal.put(":v3", 
AttributeValue.builder().s("UPDATED-other").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewMixedDotBracketPath() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Build MyHolder.subEntries = [ [map, map, map, map], [map, map, map, 
map],
+        //                               [map, map, map, map] ]
+        // Each leaf map has a single field `subField` with a positional value.
+        AttributeValue[] outer = new AttributeValue[3];
+        for (int i = 0; i < outer.length; i++) {
+            AttributeValue[] inner = new AttributeValue[4];
+            for (int j = 0; j < inner.length; j++) {
+                Map<String, AttributeValue> leaf = new HashMap<>();
+                leaf.put("subField", AttributeValue.builder().s("orig-" + i + 
"-" + j).build());
+                inner[j] = AttributeValue.builder().m(leaf).build();
+            }
+            outer[i] = AttributeValue.builder().l(inner).build();
+        }
+        Map<String, AttributeValue> seedHolder = new HashMap<>();
+        seedHolder.put("subEntries", 
AttributeValue.builder().l(outer).build());
+        seedHolder.put("otherTop", 
AttributeValue.builder().s("untouched").build());
+
+        // DDB validates that every ExpressionAttributeNames key is actually 
referenced in the
+        // expression — so the seed (which only mentions #h) needs its own 
minimal map.
+        Map<String, String> seedAttrNames = new HashMap<>();
+        seedAttrNames.put("#h", "MyHolder");
+
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET #h = :init");
+        seed.expressionAttributeNames(seedAttrNames);
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":init", AttributeValue.builder().m(seedHolder).build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        Map<String, String> exprAttrNames = new HashMap<>();
+        exprAttrNames.put("#h", "MyHolder");
+        exprAttrNames.put("#e", "subEntries");
+        exprAttrNames.put("#sf", "subField");
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET #h.#e[1][3].#sf = :v1, #h.#e[2][0].#sf = 
:v2");
+        uir.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v1", 
AttributeValue.builder().s("UPDATED-1-3").build());
+        exprAttrVal.put(":v2", 
AttributeValue.builder().s("UPDATED-2-0").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewListIndexNonZero() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Seed MyList with 6 maps so indices 2 and 4 both exist with 
unchanged neighbors.
+        AttributeValue[] initList = new AttributeValue[6];
+        for (int i = 0; i < initList.length; i++) {
+            Map<String, AttributeValue> el = new HashMap<>();
+            el.put("sku", AttributeValue.builder().s("orig" + i).build());
+            initList[i] = AttributeValue.builder().m(el).build();
+        }
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET MyList = :init");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":init", AttributeValue.builder().l(initList).build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir.updateExpression("SET MyList[2].sku = :v1, MyList[4].sku = :v2");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v1", 
AttributeValue.builder().s("UPDATED-2").build());
+        exprAttrVal.put(":v2", 
AttributeValue.builder().s("UPDATED-4").build());
+        uir.expressionAttributeValues(exprAttrVal);
+        uir.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult = 
dynamoDbClient.updateItem(uir.build());
+        UpdateItemResponse phoenixResult = 
phoenixDBClientV2.updateItem(uir.build());
+        Assert.assertEquals(dynamoResult.attributes(), 
phoenixResult.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewRemoveListIndex() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET MyList = :init");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":init", AttributeValue.builder().l(
+            AttributeValue.builder().s("a").build(),
+            AttributeValue.builder().s("b").build(),
+            AttributeValue.builder().s("c").build(),
+            AttributeValue.builder().s("d").build(),
+            AttributeValue.builder().s("e").build()).build());
+        seed.expressionAttributeValues(seedVals);
+        seed.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoSeed1 = 
dynamoDbClient.updateItem(seed.build());
+        UpdateItemResponse phoenixSeed1 = 
phoenixDBClientV2.updateItem(seed.build());
+        Assert.assertEquals(dynamoSeed1.attributes(), 
phoenixSeed1.attributes());
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("REMOVE MyList[2]");
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        UpdateItemResponse dynamoSeed2 = 
dynamoDbClient.updateItem(seed.build());
+        UpdateItemResponse phoenixSeed2 = 
phoenixDBClientV2.updateItem(seed.build());
+        Assert.assertEquals(dynamoSeed2.attributes(), 
phoenixSeed2.attributes());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("REMOVE MyList[2]");
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir3 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir3.updateExpression("REMOVE MyList[0]");
+        uir3.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult3 = 
dynamoDbClient.updateItem(uir3.build());
+        UpdateItemResponse phoenixResult3 = 
phoenixDBClientV2.updateItem(uir3.build());
+        Assert.assertEquals(dynamoResult3.attributes(), 
phoenixResult3.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir4 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir4.updateExpression("REMOVE MyList[4]");
+        uir4.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult4 = 
dynamoDbClient.updateItem(uir4.build());
+        UpdateItemResponse phoenixResult4 = 
phoenixDBClientV2.updateItem(uir4.build());
+        Assert.assertEquals(dynamoResult4.attributes(), 
phoenixResult4.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir5 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir5.updateExpression("REMOVE MyList[0]");
+        uir5.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult5 = 
dynamoDbClient.updateItem(uir5.build());
+        UpdateItemResponse phoenixResult5 = 
phoenixDBClientV2.updateItem(uir5.build());
+        Assert.assertEquals(dynamoResult5.attributes(), 
phoenixResult5.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir6 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir6.updateExpression("REMOVE MyList[4]");
+        uir6.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult6 = 
dynamoDbClient.updateItem(uir6.build());
+        UpdateItemResponse phoenixResult6 = 
phoenixDBClientV2.updateItem(uir6.build());
+        Assert.assertEquals(dynamoResult6.attributes(), 
phoenixResult6.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir7 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir7.updateExpression("REMOVE MyList[4]");
+        uir7.returnValues(ReturnValue.ALL_OLD);
+        UpdateItemResponse dynamoResult7 = 
dynamoDbClient.updateItem(uir7.build());
+        UpdateItemResponse phoenixResult7 = 
phoenixDBClientV2.updateItem(uir7.build());
+        Assert.assertEquals(dynamoResult7.attributes(), 
phoenixResult7.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir8 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir8.updateExpression("REMOVE MyList[4]");
+        uir8.returnValues(ReturnValue.ALL_NEW);
+        UpdateItemResponse dynamoResult8 = 
dynamoDbClient.updateItem(uir8.build());
+        UpdateItemResponse phoenixResult8 = 
phoenixDBClientV2.updateItem(uir8.build());
+        Assert.assertEquals(dynamoResult8.attributes(), 
phoenixResult8.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewRemoveNestedMapField() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        Map<String, AttributeValue> multiFieldMap = new HashMap<>();
+        multiFieldMap.put("field1", AttributeValue.builder().s("a").build());
+        multiFieldMap.put("field2", AttributeValue.builder().s("b").build());
+        multiFieldMap.put("field3", AttributeValue.builder().s("c").build());
+
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET MyMap = :init");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":init", 
AttributeValue.builder().m(multiFieldMap).build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression("REMOVE MyMap.field2");
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression("REMOVE MyMap.field2");
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        Map<String, AttributeValue> singleFieldMap = new HashMap<>();
+        singleFieldMap.put("onlyField", 
AttributeValue.builder().s("x").build());
+        UpdateItemRequest.Builder seedSingle =
+            UpdateItemRequest.builder().tableName(tableName).key(key);
+        seedSingle.updateExpression("SET MyMap = :init");
+        Map<String, AttributeValue> seedSingleVals = new HashMap<>();
+        seedSingleVals.put(":init", 
AttributeValue.builder().m(singleFieldMap).build());
+        seedSingle.expressionAttributeValues(seedSingleVals);
+        dynamoDbClient.updateItem(seedSingle.build());
+        phoenixDBClientV2.updateItem(seedSingle.build());
+
+        UpdateItemRequest.Builder uir3 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir3.updateExpression("REMOVE MyMap.onlyField");
+        uir3.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult3 = 
dynamoDbClient.updateItem(uir3.build());
+        UpdateItemResponse phoenixResult3 = 
phoenixDBClientV2.updateItem(uir3.build());
+        Assert.assertEquals(dynamoResult3.attributes(), 
phoenixResult3.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir4 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir4.updateExpression("REMOVE MyMap.field2");
+        uir4.returnValues(ReturnValue.ALL_OLD);
+        UpdateItemResponse dynamoResult4 = 
dynamoDbClient.updateItem(uir4.build());
+        UpdateItemResponse phoenixResult4 = 
phoenixDBClientV2.updateItem(uir4.build());
+        Assert.assertEquals(dynamoResult4.attributes(), 
phoenixResult4.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir5 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir5.updateExpression("REMOVE MyMap.field2");
+        uir5.returnValues(ReturnValue.ALL_NEW);
+        UpdateItemResponse dynamoResult5 = 
dynamoDbClient.updateItem(uir5.build());
+        UpdateItemResponse phoenixResult5 = 
phoenixDBClientV2.updateItem(uir5.build());
+        Assert.assertEquals(dynamoResult5.attributes(), 
phoenixResult5.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedOldNewRemoveDeepMixed() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        Map<String, AttributeValue> key = getKey();
+
+        // Seed MyHolder.subList = [
+        //   { leaf: { targetAttr: "X0", siblingAttr: "Z0" } },
+        //   { leaf: { targetAttr: "X1", siblingAttr: "Z1" } }
+        // ]
+        // Sibling attr means REMOVE MyHolder.subList[1].leaf.targetAttr does 
not empty
+        // the parent `leaf` map — keeps the leaf-existence vs 
parent-existence distinction sharp.
+        Map<String, AttributeValue> leaf0 = new HashMap<>();
+        leaf0.put("targetAttr", AttributeValue.builder().s("X0").build());
+        leaf0.put("siblingAttr", AttributeValue.builder().s("Z0").build());
+
+        Map<String, AttributeValue> leaf1 = new HashMap<>();
+        leaf1.put("targetAttr", AttributeValue.builder().s("X1").build());
+        leaf1.put("siblingAttr", AttributeValue.builder().s("Z1").build());
+
+        Map<String, AttributeValue> elem0 = new HashMap<>();
+        elem0.put("leaf", AttributeValue.builder().m(leaf0).build());
+        Map<String, AttributeValue> elem1 = new HashMap<>();
+        elem1.put("leaf", AttributeValue.builder().m(leaf1).build());
+
+        Map<String, AttributeValue> holder = new HashMap<>();
+        holder.put("subList", AttributeValue.builder().l(
+            AttributeValue.builder().m(elem0).build(),
+            AttributeValue.builder().m(elem1).build()).build());
+
+        UpdateItemRequest.Builder seed = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        seed.updateExpression("SET MyHolder = :init");
+        Map<String, AttributeValue> seedVals = new HashMap<>();
+        seedVals.put(":init", AttributeValue.builder().m(holder).build());
+        seed.expressionAttributeValues(seedVals);
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        // 5-segment REMOVE: MyHolder . subList [1] . leaf . targetAttr
+        final String removeExpr = "REMOVE MyHolder.subList[1].leaf.targetAttr";
+
+        UpdateItemRequest.Builder uir1 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir1.updateExpression(removeExpr);
+        uir1.returnValues(ReturnValue.UPDATED_OLD);
+        UpdateItemResponse dynamoResult1 = 
dynamoDbClient.updateItem(uir1.build());
+        UpdateItemResponse phoenixResult1 = 
phoenixDBClientV2.updateItem(uir1.build());
+        Assert.assertEquals(dynamoResult1.attributes(), 
phoenixResult1.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir2 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir2.updateExpression(removeExpr);
+        uir2.returnValues(ReturnValue.UPDATED_NEW);
+        UpdateItemResponse dynamoResult2 = 
dynamoDbClient.updateItem(uir2.build());
+        UpdateItemResponse phoenixResult2 = 
phoenixDBClientV2.updateItem(uir2.build());
+        Assert.assertEquals(dynamoResult2.attributes(), 
phoenixResult2.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir3 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir3.updateExpression(removeExpr);
+        uir3.returnValues(ReturnValue.ALL_OLD);
+        UpdateItemResponse dynamoResult3 = 
dynamoDbClient.updateItem(uir3.build());
+        UpdateItemResponse phoenixResult3 = 
phoenixDBClientV2.updateItem(uir3.build());
+        Assert.assertEquals(dynamoResult3.attributes(), 
phoenixResult3.attributes());
+
+        dynamoDbClient.updateItem(seed.build());
+        phoenixDBClientV2.updateItem(seed.build());
+
+        UpdateItemRequest.Builder uir4 = 
UpdateItemRequest.builder().tableName(tableName).key(key);
+        uir4.updateExpression(removeExpr);
+        uir4.returnValues(ReturnValue.ALL_NEW);
+        UpdateItemResponse dynamoResult4 = 
dynamoDbClient.updateItem(uir4.build());
+        UpdateItemResponse phoenixResult4 = 
phoenixDBClientV2.updateItem(uir4.build());
+        Assert.assertEquals(dynamoResult4.attributes(), 
phoenixResult4.attributes());
+
+        validateItem(tableName, key);
+    }
+
+    @Test(timeout = 120000)
+    public void testUpdatedNewEmptyUpdateRejected() {
+        final String tableName = 
testName.getMethodName().replaceAll("[\\[\\]]", "");
+        createTableAndPutItem(tableName, true);
+        UpdateItemRequest req = 
UpdateItemRequest.builder().tableName(tableName).key(getKey())
+            .returnValues(ReturnValue.UPDATED_NEW).build();
+        int dynamoStatusCode = -1;
+        int phoenixStatusCode = -1;
+        try {
+            dynamoDbClient.updateItem(req);
+            Assert.fail("Expected DDB to reject UPDATED_NEW with no update 
content");
+        } catch (DynamoDbException e) {
+            dynamoStatusCode = e.statusCode();
+        }
+        try {
+            phoenixDBClientV2.updateItem(req);
+            Assert.fail("Expected Phoenix to reject UPDATED_NEW with no update 
content");
+        } catch (DynamoDbException e) {
+            phoenixStatusCode = e.statusCode();
+        }
+        Assert.assertEquals("DDB and Phoenix should agree on status code", 
dynamoStatusCode,
+            phoenixStatusCode);
+        Assert.assertEquals("Should be 5xx", 500, phoenixStatusCode);
+    }
 }
diff --git 
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/ValidationIT.java 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/ValidationIT.java
index 30ea138..455b87e 100644
--- a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/ValidationIT.java
+++ b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/ValidationIT.java
@@ -36,8 +36,8 @@ import 
software.amazon.awssdk.core.exception.SdkServiceException;
 import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
 import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
 import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
+import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
 import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
-import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
 
 import java.sql.DriverManager;
 import java.util.HashMap;
@@ -106,12 +106,25 @@ public class ValidationIT {
         phoenixDBClientV2.createTable(createTableRequest);
         Map<String, AttributeValue> item = new HashMap<>();
         item.put("PK", AttributeValue.builder().s("key1").build());
-        Map<String, AttributeValue> key = new HashMap<>();
-        key.put("PK", AttributeValue.builder().s("key1").build());
-        UpdateItemRequest uir = 
UpdateItemRequest.builder().tableName(tableName).key(key).updateExpression("REMOVE
 COL").returnValues("UPDATED_OLD").build();
+
+        // UPDATED_OLD / UPDATED_NEW remain unsupported for PutItem (only NONE 
/ ALL_OLD apply
+        // to a write that has no "old vs new updated subset" distinction). 
UpdateItem accepts
+        // them and projects to the touched attribute paths - covered by 
UpdateItemIT tests.
+        PutItemRequest pir = 
PutItemRequest.builder().tableName(tableName).item(item)
+                .returnValues("UPDATED_OLD").build();
+        try {
+            phoenixDBClientV2.putItem(pir);
+            Assert.fail("PutItem with unsupported ReturnValue should have 
given 400 Bad Request.");
+        } catch (SdkServiceException e) {
+            Assert.assertEquals(400, e.statusCode());
+            Assert.assertTrue(e.getMessage().contains("not supported for 
ReturnValue"));
+        }
+
+        PutItemRequest pir2 = 
PutItemRequest.builder().tableName(tableName).item(item)
+                .returnValues("UPDATED_NEW").build();
         try {
-            phoenixDBClientV2.updateItem(uir);
-            Assert.fail("UpdateItem with unsupported ReturnValue should have 
given 400 Bad Request.");
+            phoenixDBClientV2.putItem(pir2);
+            Assert.fail("PutItem with unsupported ReturnValue should have 
given 400 Bad Request.");
         } catch (SdkServiceException e) {
             Assert.assertEquals(400, e.statusCode());
             Assert.assertTrue(e.getMessage().contains("not supported for 
ReturnValue"));
diff --git 
a/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/bson/BsonDocumentToMap.java
 
b/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/bson/BsonDocumentToMap.java
index 4f63fa4..ca91468 100644
--- 
a/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/bson/BsonDocumentToMap.java
+++ 
b/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/bson/BsonDocumentToMap.java
@@ -2,6 +2,7 @@ package org.apache.phoenix.ddb.bson;
 
 import java.util.ArrayList;
 import java.util.Base64;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -17,26 +18,53 @@ import org.bson.BsonValue;
 public final class BsonDocumentToMap {
 
     /**
-     * Convert the given BsonDocument into DDB item.
-     * This retrieves only the attributes provided in the list 
attributesToProject.
+     * Project the given {@link BsonDocument} down to the supplied attribute 
paths and convert the
+     * result into the ddb attribute value map.
      *
-     * @param bsonDocument The BsonDocument.
-     * @return DDB item as attribute map.
+     * @param bsonDocument        the source document
+     * @param attributesToProject paths to retain
+     * @return the DDB AttributeValue map containing only the requested 
subtrees
      */
     public static Map<String, Object> getProjectedItem(final BsonDocument 
bsonDocument,
-            List<String> attributesToProject) {
+        Collection<String> attributesToProject) {
+        return getProjectedItem(bsonDocument, attributesToProject, false);
+    }
+
+    public static Map<String, Object> getProjectedItem(final BsonDocument 
bsonDocument,
+        Collection<String> attributesToProject, boolean 
emitWholeTopLevelAttribute) {
         if (attributesToProject == null || attributesToProject.isEmpty()) {
             return getFullItem(bsonDocument);
         }
         BsonDocument newDocument = new BsonDocument();
         for (String attribute : attributesToProject) {
-            
BsonDocumentConversionUtil.updateNewBsonDocumentByFieldKeyValue(attribute, 
bsonDocument,
-                    newDocument);
+            if (emitWholeTopLevelAttribute) {
+                String topLevel = topLevelSegment(attribute);
+                if (newDocument.containsKey(topLevel)) {
+                    continue;
+                }
+                BsonDocument probe = new BsonDocument();
+                
BsonDocumentConversionUtil.updateNewBsonDocumentByFieldKeyValue(attribute,
+                    bsonDocument, probe);
+                BsonDocumentConversionUtil.removeNullListElements(probe);
+                if (probe.containsKey(topLevel)) {
+                    newDocument.put(topLevel, bsonDocument.get(topLevel));
+                }
+            } else {
+                
BsonDocumentConversionUtil.updateNewBsonDocumentByFieldKeyValue(attribute,
+                    bsonDocument, newDocument);
+            }
         }
         BsonDocumentConversionUtil.removeNullListElements(newDocument);
         return getFullItem(newDocument);
     }
 
+    private static String topLevelSegment(String path) {
+        int dot = path.indexOf('.');
+        int bracket = path.indexOf('[');
+        int boundary = dot < 0 ? bracket : (bracket < 0 ? dot : Math.min(dot, 
bracket));
+        return boundary < 0 ? path : path.substring(0, boundary);
+    }
+
     public static Map<String, Object> getFullItem(BsonDocument document) {
         Map<String, Object> map = new HashMap<>();
         updateMapWithDocAttributes(map, document);
diff --git 
a/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/utils/CommonServiceUtils.java
 
b/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/utils/CommonServiceUtils.java
index b2027e9..debc393 100644
--- 
a/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/utils/CommonServiceUtils.java
+++ 
b/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/utils/CommonServiceUtils.java
@@ -36,11 +36,14 @@ import org.apache.phoenix.schema.types.PVarchar;
 import org.bson.BsonDocument;
 import org.bson.BsonNull;
 import org.bson.BsonString;
+import org.bson.BsonValue;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -56,6 +59,10 @@ public class CommonServiceUtils {
     private static final RawBsonDocument EMPTY_RAW_BSON_DOC = 
RawBsonDocument.parse("{}");
     private static final Logger LOGGER = 
LoggerFactory.getLogger(CommonServiceUtils.class);
 
+    private static final String[] UPDATE_DOC_CLAUSES = {
+        "$SET", "$UNSET", "$ADD", "$DELETE_FROM_SET"
+    };
+
     public static boolean isCauseMessageAvailable(Exception e) {
         return e.getCause() != null && e.getCause().getMessage() != null;
     }
@@ -155,6 +162,20 @@ public class CommonServiceUtils {
                 MapToBsonDocument.getBsonDocument(exprAttrVals), 
exprAttrNames);
     }
 
+    public static Set<String> getTouchedPathsFromUpdateDoc(BsonDocument 
updateDoc) {
+        Set<String> paths = new HashSet<>();
+        if (updateDoc == null) {
+            return paths;
+        }
+        for (String clauseKey : UPDATE_DOC_CLAUSES) {
+            BsonValue clause = updateDoc.get(clauseKey);
+            if (clause != null && clause.isDocument()) {
+                paths.addAll(clause.asDocument().keySet());
+            }
+        }
+        return paths;
+    }
+
     /**
      * Enclose given argument within double quotes. This is used to escape 
column/table names in queries.
      */

Reply via email to