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 fb15989 Evaluate udpate expression on empty document to create VALUES
entity
fb15989 is described below
commit fb159897018cc79a202b5c74cf9aa80b7c6faa02
Author: Palash Chauhan <[email protected]>
AuthorDate: Wed Feb 18 11:08:30 2026 -0800
Evaluate udpate expression on empty document to create VALUES entity
---
.../phoenix/ddb/service/DeleteItemService.java | 2 +-
.../apache/phoenix/ddb/service/PutItemService.java | 2 +-
.../phoenix/ddb/service/UpdateItemService.java | 57 ++++---
.../apache/phoenix/ddb/service/utils/DMLUtils.java | 19 ++-
.../apache/phoenix/ddb/UpdateItemBaseTests.java | 171 +++++++++++++++++++++
5 files changed, 226 insertions(+), 25 deletions(-)
diff --git
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/DeleteItemService.java
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/DeleteItemService.java
index 8892ffc..68262ff 100644
---
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/DeleteItemService.java
+++
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/DeleteItemService.java
@@ -78,7 +78,7 @@ public class DeleteItemService {
return DMLUtils.executeUpdate(stmtInfo.stmt,
(String) request.get(ApiMetadata.RETURN_VALUES),
(String)
request.get(ApiMetadata.RETURN_VALUES_ON_CONDITION_CHECK_FAILURE),
- hasCondExp, pkCols, ApiOperation.DELETE_ITEM);
+ hasCondExp, true, pkCols, ApiOperation.DELETE_ITEM);
}
/**
diff --git
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/PutItemService.java
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/PutItemService.java
index 97751d7..ef51173 100644
---
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/PutItemService.java
+++
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/PutItemService.java
@@ -89,7 +89,7 @@ public class PutItemService {
return DMLUtils.executeUpdate(stmtInfo.stmt,
(String) request.get(ApiMetadata.RETURN_VALUES),
(String)
request.get(ApiMetadata.RETURN_VALUES_ON_CONDITION_CHECK_FAILURE),
- hasCondExp, pkCols, ApiOperation.PUT_ITEM);
+ hasCondExp, true, pkCols, ApiOperation.PUT_ITEM);
}
private static StatementInfo getPreparedStatement(Connection conn,
Map<String, Object> request,
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 5c3a412..20a587f 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
@@ -7,6 +7,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+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;
@@ -89,7 +91,7 @@ public class UpdateItemService {
Map<String, Object> res =
DMLUtils.executeUpdate(statementInfo.stmt,
(String) request.get(ApiMetadata.RETURN_VALUES),
(String)
request.get(ApiMetadata.RETURN_VALUES_ON_CONDITION_CHECK_FAILURE),
- hasCondExp, pkCols, ApiOperation.UPDATE_ITEM);
+ hasCondExp, statementInfo.canEvaluateUpdateExprOnEmptyDoc,
pkCols, ApiOperation.UPDATE_ITEM);
res.put(ApiMetadata.CONSUMED_CAPACITY,
CommonServiceUtils.getConsumedCapacity((String)request.get(ApiMetadata.TABLE_NAME)));
return res;
@@ -136,11 +138,17 @@ public class UpdateItemService {
}
// Extract $SET and $ADD portion from updateDoc for VALUES() clause
- BsonDocument newItemDoc = extractSetAndAddDocument(updateDoc, request,
pkCols);
+ boolean canEvaluateUpdateExprOnEmptyDoc = true;
+ BsonDocument newItemDoc = new BsonDocument();
+ try {
+ updateNewItemDoc(newItemDoc, updateDoc, request, pkCols);
+ } catch (BsonUpdateInvalidArgumentException e) {
+ canEvaluateUpdateExprOnEmptyDoc = false;
+ }
// Determine query format to use
QueryFormatInfo formatInfo =
- determineQueryFormat(condExpr, exprAttrNames, pkCols.size());
+ determineQueryFormat(condExpr, exprAttrNames, pkCols.size(),
canEvaluateUpdateExprOnEmptyDoc);
BsonDocument conditionDoc = null;
if (!StringUtils.isEmpty(condExpr)) {
@@ -150,17 +158,19 @@ public class UpdateItemService {
PreparedStatement stmt =
conn.prepareStatement(String.format(formatInfo.queryFormat,
PhoenixUtils.getFullTableName(tableName, true)));
- return new StatementInfo(stmt, conditionDoc, updateDoc, newItemDoc,
formatInfo.needsValuesDoc);
+ return new StatementInfo(stmt, conditionDoc, updateDoc, newItemDoc,
formatInfo.needsValuesDoc, canEvaluateUpdateExprOnEmptyDoc);
}
/**
- * Extract values from the update document to use in VALUES() clause for
new row creation.
+ * Extract values from the update document and apply on an empty document
to use in VALUES() clause for new row creation.
* This includes SET operations, ADD operations (for new item creation),
and primary keys.
* For DynamoDB compatibility, ADD operations should contribute to initial
values when creating new items.
*/
- private static BsonDocument extractSetAndAddDocument(BsonDocument
updateDoc,
- Map<String, Object> request, List<PColumn> pkCols) {
- BsonDocument newItemDoc = new BsonDocument();
+ private static void updateNewItemDoc(BsonDocument newItemDoc,
+ BsonDocument updateDoc,
+ Map<String, Object> request,
+ List<PColumn> pkCols) {
+ BsonDocument newUpdateDoc = new BsonDocument();
// Add primary key values to the set document
addKeysToNewItemDoc(newItemDoc, request, pkCols);
@@ -169,21 +179,20 @@ public class UpdateItemService {
// Add SET operations - these always contribute to new item
creation
if (updateDoc.containsKey("$SET")) {
BsonDocument setBsonDoc = updateDoc.getDocument("$SET");
- newItemDoc.putAll(setBsonDoc);
+ newUpdateDoc.put("$SET", setBsonDoc);
}
// Add ADD operations - for new item creation, these become
initial values
// DynamoDB semantics: ADD on non-existing item creates item with
ADD value
if (updateDoc.containsKey("$ADD")) {
BsonDocument addBsonDoc = updateDoc.getDocument("$ADD");
- newItemDoc.putAll(addBsonDoc);
+ newUpdateDoc.put("$ADD", addBsonDoc);
}
// Note: REMOVE and DELETE operations don't contribute to new item
creation
// They are no-ops on non-existing items, handled by the update
expression
}
-
- return newItemDoc;
+ UpdateExpressionUtils.updateExpression(newUpdateDoc, newItemDoc);
}
/**
@@ -232,7 +241,7 @@ public class UpdateItemService {
* Determine the appropriate query format based on conditions and
operations.
*/
private static QueryFormatInfo determineQueryFormat(String condExpr,
- Map<String, String> exprAttrNames, int pkColsSize) {
+ Map<String, String> exprAttrNames, int pkColsSize, boolean
canEvaluateUpdateExprOnEmptyDoc) {
boolean hasCondition = !StringUtils.isEmpty(condExpr);
boolean canCreateNewItemWithCondition = false;
@@ -242,7 +251,7 @@ public class UpdateItemService {
canCreateNewItemWithCondition =
evaluateConditionOnNonExistingItem(condExpr, exprAttrNames);
}
- if (canCreateNewItemWithCondition) {
+ if (canCreateNewItemWithCondition && canEvaluateUpdateExprOnEmptyDoc) {
// Can create new item and have values to insert (set/add or even
just keys)
String format = pkColsSize == 1 ?
CONDITIONAL_UPDATE_WITH_HASH_KEY :
@@ -257,10 +266,17 @@ public class UpdateItemService {
return new QueryFormatInfo(format, false); // UPDATE_ONLY
doesn't use VALUES document
} else {
// there was no condition to begin with, still allow creation
- String format = (pkColsSize == 1) ?
- UPDATE_WITH_HASH_KEY :
- UPDATE_WITH_HASH_SORT_KEY;
- return new QueryFormatInfo(format, true);
+ if (canEvaluateUpdateExprOnEmptyDoc) {
+ String format = (pkColsSize == 1) ?
+ UPDATE_WITH_HASH_KEY :
+ UPDATE_WITH_HASH_SORT_KEY;
+ return new QueryFormatInfo(format, true);
+ } else {
+ String format = (pkColsSize == 1) ?
+ UPDATE_ONLY_WITH_HASH_KEY :
+ UPDATE_ONLY_WITH_HASH_SORT_KEY;
+ return new QueryFormatInfo(format, false);
+ }
}
}
}
@@ -317,14 +333,17 @@ public class UpdateItemService {
final BsonDocument updateDoc;
final BsonDocument newItemDoc;
final boolean needsValuesDoc;
+ final boolean canEvaluateUpdateExprOnEmptyDoc;
public StatementInfo(PreparedStatement stmt, BsonDocument conditionDoc,
- BsonDocument updateDoc, BsonDocument newItemDoc, boolean
needsValuesDoc) {
+ BsonDocument updateDoc, BsonDocument newItemDoc, boolean
needsValuesDoc,
+ boolean canEvaluateUpdateExprOnEmptyDoc) {
this.stmt = stmt;
this.conditionDoc = conditionDoc;
this.updateDoc = updateDoc;
this.newItemDoc = newItemDoc;
this.needsValuesDoc = needsValuesDoc;
+ this.canEvaluateUpdateExprOnEmptyDoc =
canEvaluateUpdateExprOnEmptyDoc;
}
}
}
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 413150a..f03ca3a 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
@@ -63,14 +63,21 @@ public class DMLUtils {
* TODO: UPDATED_OLD | UPDATED_NEW
*/
public static Map<String, Object> executeUpdate(PreparedStatement stmt,
String returnValue,
- String returnValuesOnConditionCheckFailure, boolean hasCondExp,
List<PColumn> pkCols,
- ApiOperation apiOperation) throws SQLException,
ConditionCheckFailedException {
+ String returnValuesOnConditionCheckFailure, boolean hasCondExp,
boolean canEvaluateUpdateExprOnEmptyDoc,
+ List<PColumn> pkCols,
ApiOperation apiOperation)
+ throws SQLException, ConditionCheckFailedException
{
try {
Map<String, Object> returnAttrs = new HashMap<>();
if (!needReturnRow(returnValue,
returnValuesOnConditionCheckFailure)) {
int returnStatus = stmt.executeUpdate();
- if (returnStatus == 0 && hasCondExp) {
- throw new ConditionCheckFailedException();
+ if (returnStatus == 0) {
+ if (hasCondExp) {
+ throw new ConditionCheckFailedException();
+ }
+ if (!canEvaluateUpdateExprOnEmptyDoc && apiOperation ==
ApiOperation.UPDATE_ITEM) {
+ throw new ValidationException(
+ "The provided expression references an
attribute that does not exist in the item");
+ }
}
return new HashMap<>();
}
@@ -101,6 +108,10 @@ public class DMLUtils {
}
throw conditionalCheckFailedException;
}
+ if (!canEvaluateUpdateExprOnEmptyDoc && apiOperation ==
ApiOperation.UPDATE_ITEM) {
+ throw new ValidationException(
+ "The provided expression references an attribute
that does not exist in the item");
+ }
} else {
boolean returnValuesInResponse = false;
if (apiOperation != ApiOperation.DELETE_ITEM) {
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 75de539..6e650ac 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
@@ -63,6 +63,7 @@ import org.apache.phoenix.util.ServerUtil;
import static org.apache.phoenix.query.BaseTest.setUpConfigForMiniCluster;
import static
software.amazon.awssdk.services.dynamodb.model.ReturnValue.ALL_NEW;
+import static
software.amazon.awssdk.services.dynamodb.model.ReturnValue.ALL_OLD;
/**
* Tests for UpdateItem API without conditional updates.
@@ -403,6 +404,175 @@ public class UpdateItemBaseTests {
validateItem(tableName, key);
}
+ @Test(timeout = 120000)
+ public void testCounterIncrement1() {
+ final String tableName =
testName.getMethodName().replaceAll("[\\[\\]]", "");
+ createTableAndPutItem(tableName, false);
+
+ // update item
+ Map<String, AttributeValue> key = getKey();
+ UpdateItemRequest.Builder uir =
UpdateItemRequest.builder().tableName(tableName).key(key);
+ uir.updateExpression(
+ "SET #counter = if_not_exists(#counter, :start) + :increment");
+ Map<String, String> exprAttrNames = new HashMap<>();
+ exprAttrNames.put("#counter", "Counter");
+ uir.expressionAttributeNames(exprAttrNames);
+ Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+ exprAttrVal.put(":start", AttributeValue.builder().n("1").build());
+ exprAttrVal.put(":increment", AttributeValue.builder().n("1").build());
+ uir.expressionAttributeValues(exprAttrVal);
+ uir.returnValues(ALL_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 testCounterIncrement2() {
+ final String tableName =
testName.getMethodName().replaceAll("[\\[\\]]", "");
+ createTableAndPutItem(tableName, true);
+
+ // update item
+ Map<String, AttributeValue> key = getKey();
+ UpdateItemRequest.Builder uir =
UpdateItemRequest.builder().tableName(tableName).key(key);
+ uir.updateExpression(
+ "SET #counter = if_not_exists(#counter, :start) + :increment");
+ Map<String, String> exprAttrNames = new HashMap<>();
+ exprAttrNames.put("#counter", "Counter");
+ uir.expressionAttributeNames(exprAttrNames);
+ Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+ exprAttrVal.put(":start", AttributeValue.builder().n("1").build());
+ exprAttrVal.put(":increment", AttributeValue.builder().n("1").build());
+ uir.expressionAttributeValues(exprAttrVal);
+ uir.returnValues(ALL_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 testCounterIncrement3() {
+ final String tableName =
testName.getMethodName().replaceAll("[\\[\\]]", "");
+ createTableAndPutItem(tableName, false);
+
+ // update item
+ Map<String, AttributeValue> key = getKey();
+ UpdateItemRequest.Builder uir =
UpdateItemRequest.builder().tableName(tableName).key(key);
+ uir.updateExpression(
+ "SET #counter = if_not_exists(#counter, :start) + :increment");
+ Map<String, String> exprAttrNames = new HashMap<>();
+ exprAttrNames.put("#counter", "Counter");
+ uir.expressionAttributeNames(exprAttrNames);
+ Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+ exprAttrVal.put(":start", AttributeValue.builder().n("1").build());
+ exprAttrVal.put(":increment", AttributeValue.builder().n("1").build());
+ uir.expressionAttributeValues(exprAttrVal);
+ uir.returnValues(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 testCounterIncrement4() {
+ final String tableName =
testName.getMethodName().replaceAll("[\\[\\]]", "");
+ createTableAndPutItem(tableName, true);
+
+ // update item
+ Map<String, AttributeValue> key = getKey();
+ UpdateItemRequest.Builder uir =
UpdateItemRequest.builder().tableName(tableName).key(key);
+ uir.updateExpression(
+ "SET #counter = if_not_exists(#counter, :start) + :increment");
+ Map<String, String> exprAttrNames = new HashMap<>();
+ exprAttrNames.put("#counter", "Counter");
+ uir.expressionAttributeNames(exprAttrNames);
+ Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+ exprAttrVal.put(":start", AttributeValue.builder().n("1").build());
+ exprAttrVal.put(":increment", AttributeValue.builder().n("1").build());
+ uir.expressionAttributeValues(exprAttrVal);
+ uir.returnValues(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 testDeleteRemoveSetAddNonExistentItemSuccess() {
+ final String tableName =
testName.getMethodName().replaceAll("[\\[\\]]", "");
+ createTableAndPutItem(tableName, false);
+
+ // update item
+ Map<String, AttributeValue> key = getKey();
+ UpdateItemRequest.Builder uir =
UpdateItemRequest.builder().tableName(tableName).key(key);
+ uir.updateExpression(
+ "DELETE #1 :v1 REMOVE #2 ADD #3 :v3 SET #5 = if_not_exists(#5,
:v5) + :v51, #4 = :v4");
+ Map<String, String> exprAttrNames = new HashMap<>();
+ exprAttrNames.put("#1", "TopLevelSet");
+ exprAttrNames.put("#2", "Reviews");
+ exprAttrNames.put("#3", "COL1");
+ exprAttrNames.put("#4", "COL3");
+ exprAttrNames.put("#5", "NEWCOL");
+ uir.expressionAttributeNames(exprAttrNames);
+ Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+ exprAttrVal.put(":v1",
AttributeValue.builder().ss("setMember2").build());
+ exprAttrVal.put(":v3", AttributeValue.builder().n("1000000").build());
+ exprAttrVal.put(":v4",
AttributeValue.builder().s("dEsCrIpTiOn1").build());
+ exprAttrVal.put(":v5", AttributeValue.builder().n("1").build());
+ exprAttrVal.put(":v51", AttributeValue.builder().n("10").build());
+ uir.expressionAttributeValues(exprAttrVal);
+ uir.returnValues(ALL_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 testDeleteRemoveSetAddNonExistentItemFail() {
+ final String tableName =
testName.getMethodName().replaceAll("[\\[\\]]", "");
+ createTableAndPutItem(tableName, false);
+
+ // update item
+ Map<String, AttributeValue> key = getKey();
+ UpdateItemRequest.Builder uir =
UpdateItemRequest.builder().tableName(tableName).key(key);
+ uir.updateExpression(
+ "DELETE #1 :v1 REMOVE #2 ADD #3 :v3 SET #5 = #5 + :v5, #4 =
:v4");
+ Map<String, String> exprAttrNames = new HashMap<>();
+ exprAttrNames.put("#1", "TopLevelSet");
+ exprAttrNames.put("#2", "Reviews");
+ exprAttrNames.put("#3", "COL1");
+ exprAttrNames.put("#4", "COL3");
+ exprAttrNames.put("#5", "NEWCOL");
+ uir.expressionAttributeNames(exprAttrNames);
+ Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+ exprAttrVal.put(":v1",
AttributeValue.builder().ss("setMember2").build());
+ exprAttrVal.put(":v3", AttributeValue.builder().n("1000000").build());
+ exprAttrVal.put(":v4",
AttributeValue.builder().s("dEsCrIpTiOn1").build());
+ exprAttrVal.put(":v5", AttributeValue.builder().n("1").build());
+ uir.expressionAttributeValues(exprAttrVal);
+ uir.returnValues(ALL_NEW);
+ try {
+ UpdateItemResponse dynamoResult =
dynamoDbClient.updateItem(uir.build());
+ } catch (DynamoDbException e) {
+ Assert.assertEquals(400, e.statusCode());
+ }
+ try {
+ UpdateItemResponse phoenixResult =
phoenixDBClientV2.updateItem(uir.build());
+ } catch (DynamoDbException e) {
+ Assert.assertEquals(400, e.statusCode());
+ }
+ validateItem(tableName, key);
+ }
+
protected void createTableAndPutItem(String tableName, boolean putItem) {
//create table
CreateTableRequest createTableRequest;
@@ -445,6 +615,7 @@ public class UpdateItemBaseTests {
item.put("COL4", AttributeValue.builder().n("34").build());
item.put("COL5", AttributeValue.builder().n("67").build());
item.put("TopLevelSet",
AttributeValue.builder().ss("setMember1").build());
+ item.put("Counter", AttributeValue.builder().n("66").build());
Map<String, AttributeValue> reviewMap1 = new HashMap<>();
reviewMap1.put("reviewer",
AttributeValue.builder().s("Alice").build());
Map<String, AttributeValue> fiveStarMap = new HashMap<>();