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.
*/