This is an automated email from the ASF dual-hosted git repository.
palashc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/master by this push:
new 5ed966711c PHOENIX-7757 : Support arithmetic with if_not_exists in
bson SET update expression (#2362)
5ed966711c is described below
commit 5ed966711cec540e3a0c8160a083bb7c64f601fa
Author: Palash Chauhan <[email protected]>
AuthorDate: Tue Feb 3 15:04:45 2026 -0800
PHOENIX-7757 : Support arithmetic with if_not_exists in bson SET update
expression (#2362)
Co-authored-by: Palash Chauhan
<[email protected]>
---
.../util/bson/UpdateExpressionUtils.java | 72 ++++++--
.../util/bson/UpdateExpressionUtilsTest.java | 199 +++++++++++++++++++++
2 files changed, 260 insertions(+), 11 deletions(-)
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java
index 4b3b64b4fb..a3731819c4 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java
@@ -585,7 +585,9 @@ public class UpdateExpressionUtils {
* arithmetic operation is performed. If the current value is a bson
document with an entry from
* $IF_NOT_EXISTS to a document with a key and a fallback value, we lookup
if the key is already
* present in the document. If it is, we return its value. Otherwise, we
return the provided
- * fallback value.
+ * fallback value. If the current value is a bson document with $ADD or
$SUBTRACT as key, we get
+ * the array of operands from this document and perform the corresponding
operation. Operand can
+ * be an $IF_NOT_EXISTS bson document.
* @param curValue The current value.
* @param bsonDocument The document with all field key-value pairs.
* @return Updated values to be used by SET operation.
@@ -638,19 +640,67 @@ public class UpdateExpressionUtils {
}
}
return getBsonNumberFromNumber(newNum);
+ } else if (curValue instanceof BsonDocument) {
+ BsonDocument doc = (BsonDocument) curValue;
+ if (doc.get("$IF_NOT_EXISTS") != null) {
+ return resolveIfNotExists(doc, bsonDocument);
+ } else if (doc.containsKey("$ADD")) {
+ BsonArray operands = doc.getArray("$ADD");
+ Number result = resolveSetOperand(operands.get(0), bsonDocument);
+ result = addNum(result, resolveSetOperand(operands.get(1),
bsonDocument));
+ return getBsonNumberFromNumber(result);
+ } else if (doc.containsKey("$SUBTRACT")) {
+ BsonArray operands = doc.getArray("$SUBTRACT");
+ Number result = resolveSetOperand(operands.get(0), bsonDocument);
+ result = subtractNum(result, resolveSetOperand(operands.get(1),
bsonDocument));
+ return getBsonNumberFromNumber(result);
+ }
+ }
+ return curValue;
+ }
+
+ /**
+ * Resolves an $IF_NOT_EXISTS expression. Returns the existing field value
if present, otherwise
+ * returns the fallback value.
+ * @param ifNotExistsDoc The document containing the $IF_NOT_EXISTS
expression
+ * @param bsonDocument The source document to look up field values
+ * @return The resolved value (existing field value or fallback)
+ */
+ private static BsonValue resolveIfNotExists(final BsonDocument
ifNotExistsDoc,
+ final BsonDocument bsonDocument) {
+ BsonValue ifNotExistsSpec = ifNotExistsDoc.get("$IF_NOT_EXISTS");
+ Map.Entry<String, BsonValue> ifNotExistsEntry =
+ ((BsonDocument) ifNotExistsSpec).entrySet().iterator().next();
+ String fieldPath = ifNotExistsEntry.getKey();
+ BsonValue fallbackValue = ifNotExistsEntry.getValue();
+ BsonValue existingValue =
+ CommonComparisonExpressionUtils.getFieldFromDocument(fieldPath,
bsonDocument);
+ return (existingValue != null) ? existingValue : fallbackValue;
+ }
+
+ private static Number resolveSetOperand(BsonValue operand, BsonDocument
bsonDocument) {
+ if (operand.isNumber()) {
+ return getNumberFromBsonNumber((BsonNumber) operand);
} else if (
- curValue instanceof BsonDocument && ((BsonDocument)
curValue).get("$IF_NOT_EXISTS") != null
+ operand instanceof BsonDocument && ((BsonDocument)
operand).get("$IF_NOT_EXISTS") != null
) {
- BsonValue ifNotExistsDoc = ((BsonDocument)
curValue).get("$IF_NOT_EXISTS");
- Map.Entry<String, BsonValue> ifNotExistsEntry =
- ((BsonDocument) ifNotExistsDoc).entrySet().iterator().next();
- String ifNotExistsKey = ifNotExistsEntry.getKey();
- BsonValue ifNotExistsVal = ifNotExistsEntry.getValue();
- BsonValue val =
- CommonComparisonExpressionUtils.getFieldFromDocument(ifNotExistsKey,
bsonDocument);
- return (val != null) ? val : ifNotExistsVal;
+ BsonValue resolved = resolveIfNotExists((BsonDocument) operand,
bsonDocument);
+ return getNumberFromBsonNumber((BsonNumber) resolved);
+ } else if (operand instanceof BsonString) {
+ String operandVal = ((BsonString) operand).getValue();
+ BsonValue topLevelValue = bsonDocument.get(operandVal);
+ BsonValue bsonValue = topLevelValue != null
+ ? topLevelValue
+ : CommonComparisonExpressionUtils.getFieldFromDocument(operandVal,
bsonDocument);
+ if (bsonValue != null && bsonValue.isNumber()) {
+ return getNumberFromBsonNumber((BsonNumber) bsonValue);
+ } else {
+ throw new BsonUpdateInvalidArgumentException(
+ "Operand for $SET not found in document: " + operand);
+ }
+ } else {
+ throw new BsonUpdateInvalidArgumentException("Invalid operand for $SET:
" + operand);
}
- return curValue;
}
/**
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionUtilsTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionUtilsTest.java
index 38c07def73..062e1ac813 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionUtilsTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionUtilsTest.java
@@ -1065,4 +1065,203 @@ public class UpdateExpressionUtilsTest {
return RawBsonDocument.parse(json);
}
+ @Test
+ public void testArithmeticWithIfNotExists() {
+ String initialDocJson = "{\n" + " \"existingCounter\": 100,\n" + "
\"anotherCounter\": 50,\n"
+ + " \"updateCount\": 25,\n" + " \"name\": \"test\"\n" + "}";
+ BsonDocument bsonDocument = BsonDocument.parse(initialDocJson);
+
+ // Test Case 1: if_not_exists with non-existent field (should use fallback
0) + 5 = 5
+ // Test Case 2: if_not_exists with existing field (existingCounter=100) +
10 = 110
+ // Test Case 3: 20 + if_not_exists(missingField, 30) = 20 + 30 = 50
+ // Test Case 4: if_not_exists(anotherCounter=50, 0) -
if_not_exists(missing, 10) = 50 - 10 = 40
+ // Test Case 5: updateCount = if_not_exists(updateCount=25, 0) + 1 = 26
+ // Test Case 6: newCounter = if_not_exists(newCounter, 0) + 1 = 0 + 1 = 1
(field doesn't exist)
+ String updateExpression =
+ "{\n" + " \"$SET\": {\n" + " \"newFieldFromFallback\": {\n" + "
\"$ADD\": [\n"
+ + " {\"$IF_NOT_EXISTS\": {\"nonExistentField\": 0}},\n" + "
5\n" + " ]\n"
+ + " },\n" + " \"existingFieldIncrement\": {\n" + "
\"$ADD\": [\n"
+ + " {\"$IF_NOT_EXISTS\": {\"existingCounter\": 0}},\n" + "
10\n" + " ]\n"
+ + " },\n" + " \"reversedOperands\": {\n" + " \"$ADD\": [\n"
+ " 20,\n"
+ + " {\"$IF_NOT_EXISTS\": {\"missingField\": 30}}\n" + "
]\n" + " },\n"
+ + " \"bothIfNotExists\": {\n" + " \"$SUBTRACT\": [\n"
+ + " {\"$IF_NOT_EXISTS\": {\"anotherCounter\": 0}},\n"
+ + " {\"$IF_NOT_EXISTS\": {\"missingCounter\": 10}}\n" + "
]\n" + " },\n"
+ + " \"updateCount\": {\n" + " \"$ADD\": [\n"
+ + " {\"$IF_NOT_EXISTS\": {\"updateCount\": 0}},\n" + "
1\n" + " ]\n"
+ + " },\n" + " \"newCounter\": {\n" + " \"$ADD\": [\n"
+ + " {\"$IF_NOT_EXISTS\": {\"newCounter\": 0}},\n" + "
1\n" + " ]\n"
+ + " }\n" + " }\n" + "}";
+
+ RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+ UpdateExpressionUtils.updateExpression(expressionDoc, bsonDocument);
+
+ // Verify results
+ // Case 1: nonExistentField doesn't exist, so fallback 0 + 5 = 5
+ Assert.assertEquals(5,
bsonDocument.getInt32("newFieldFromFallback").getValue());
+
+ // Case 2: existingCounter exists with value 100, so 100 + 10 = 110
+ Assert.assertEquals(110,
bsonDocument.getInt32("existingFieldIncrement").getValue());
+
+ // Case 3: 20 + if_not_exists(missingField, 30) = 20 + 30 = 50
+ Assert.assertEquals(50,
bsonDocument.getInt32("reversedOperands").getValue());
+
+ // Case 4: if_not_exists(anotherCounter=50, 0) -
if_not_exists(missingCounter, 10) = 50 - 10 =
+ // 40
+ Assert.assertEquals(40,
bsonDocument.getInt32("bothIfNotExists").getValue());
+
+ // Case 5: updateCount = if_not_exists(updateCount=25, 0) + 1 = 26
+ Assert.assertEquals(26, bsonDocument.getInt32("updateCount").getValue());
+
+ // Case 6: newCounter = if_not_exists(newCounter, 0) + 1 = 0 + 1 = 1
+ Assert.assertEquals(1, bsonDocument.getInt32("newCounter").getValue());
+
+ // Verify original fields are unchanged
+ Assert.assertEquals(100,
bsonDocument.getInt32("existingCounter").getValue());
+ Assert.assertEquals(50,
bsonDocument.getInt32("anotherCounter").getValue());
+ Assert.assertEquals("test", bsonDocument.getString("name").getValue());
+ }
+
+ /**
+ * Test arithmetic with $IF_NOT_EXISTS on nested document paths.
+ */
+ @Test
+ public void testArithmeticWithIfNotExistsNestedPaths() {
+ String initialDocJson = "{\n" + " \"stats\": {\n" + " \"viewCount\":
100,\n"
+ + " \"nested\": {\n" + " \"deepCounter\": 500\n" + " }\n" + "
},\n"
+ + " \"items\": [10, 20, 30]\n" + "}";
+ BsonDocument bsonDocument = BsonDocument.parse(initialDocJson);
+
+ // Test nested path: stats.viewCount exists (100) + 1 = 101
+ // Test nested path: stats.likeCount doesn't exist, fallback 0 + 5 = 5
+ // Test deep nested: stats.nested.deepCounter exists (500) + 100 = 600
+ // Test array element: items[1] exists (20) + 5 = 25
+ String updateExpression = "{\n" + " \"$SET\": {\n" + "
\"stats.viewCount\": {\n"
+ + " \"$ADD\": [\n" + " {\"$IF_NOT_EXISTS\":
{\"stats.viewCount\": 0}},\n"
+ + " 1\n" + " ]\n" + " },\n" + " \"stats.likeCount\":
{\n"
+ + " \"$ADD\": [\n" + " {\"$IF_NOT_EXISTS\":
{\"stats.likeCount\": 0}},\n"
+ + " 5\n" + " ]\n" + " },\n" + "
\"stats.nested.deepCounter\": {\n"
+ + " \"$ADD\": [\n" + " {\"$IF_NOT_EXISTS\":
{\"stats.nested.deepCounter\": 0}},\n"
+ + " 100\n" + " ]\n" + " },\n" + " \"items[1]\": {\n" +
" \"$ADD\": [\n"
+ + " {\"$IF_NOT_EXISTS\": {\"items[1]\": 0}},\n" + " 5\n" +
" ]\n"
+ + " }\n" + " }\n" + "}";
+
+ RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+ UpdateExpressionUtils.updateExpression(expressionDoc, bsonDocument);
+
+ // Verify nested path results
+ BsonDocument stats = bsonDocument.getDocument("stats");
+ Assert.assertEquals(101, stats.getInt32("viewCount").getValue());
+ Assert.assertEquals(5, stats.getInt32("likeCount").getValue());
+ Assert.assertEquals(600,
stats.getDocument("nested").getInt32("deepCounter").getValue());
+
+ // Verify array element
+ Assert.assertEquals(25,
bsonDocument.getArray("items").get(1).asInt32().getValue());
+ }
+
+ /**
+ * Test arithmetic with $IF_NOT_EXISTS using decimal/double values.
+ */
+ @Test
+ public void testArithmeticWithIfNotExistsDecimalValues() {
+ String initialDocJson = "{\n" + " \"price\": 99.99,\n" + " \"quantity\":
5\n" + "}";
+ BsonDocument bsonDocument = BsonDocument.parse(initialDocJson);
+
+ // Test with decimal values
+ // price exists (99.99) + 0.01 = 100.0
+ // discount doesn't exist, fallback 0.0 + 10.5 = 10.5
+ // mixed: quantity (int 5) + 2.5 = 7.5
+ String updateExpression = "{\n" + " \"$SET\": {\n" + " \"price\": {\n"
+ + " \"$ADD\": [\n" + " {\"$IF_NOT_EXISTS\": {\"price\":
0.0}},\n"
+ + " 0.01\n" + " ]\n" + " },\n" + " \"discount\": {\n"
+ + " \"$ADD\": [\n" + " {\"$IF_NOT_EXISTS\": {\"discount\":
0.0}},\n"
+ + " 10.5\n" + " ]\n" + " },\n" + " \"total\": {\n" + "
\"$ADD\": [\n"
+ + " {\"$IF_NOT_EXISTS\": {\"quantity\": 0}},\n" + " 2.5\n"
+ " ]\n"
+ + " }\n" + " }\n" + "}";
+
+ RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+ UpdateExpressionUtils.updateExpression(expressionDoc, bsonDocument);
+
+ // Verify decimal results
+ Assert.assertEquals(100.0, bsonDocument.getDouble("price").getValue(),
0.001);
+ Assert.assertEquals(10.5, bsonDocument.getDouble("discount").getValue(),
0.001);
+ Assert.assertEquals(7.5, bsonDocument.getDouble("total").getValue(),
0.001);
+ }
+
+ @Test
+ public void testMixedSetExpressions() {
+ String initialDocJson = "{\n" + " \"fieldA\": 10,\n" + " \"fieldB\":
25,\n"
+ + " \"existingValue\": \"will be overwritten\",\n" + " \"items\":
[100, 200, 300],\n"
+ + " \"counter\": 50,\n" + " \"nested\": {\"value\": 5},\n" + "
\"a.b\": 7\n" + "}";
+ BsonDocument bsonDocument = BsonDocument.parse(initialDocJson);
+
+ String updateExpression = "{\n" + " \"$SET\": {\n"
+ // 1. Simple key = value
+ + " \"simpleField\": \"newValue\",\n" + " \"numericField\": 42,\n"
+ // 2. string-based arithmetic: fieldA + fieldB = 10 + 25 = 35
+ + " \"sumField\": \"fieldA + fieldB\",\n"
+ // 2b. string-based arithmetic with subtraction: fieldB - fieldA = 25 -
10 = 15
+ + " \"diffField\": \"fieldB - fieldA\",\n"
+ // 3. Array element set: items[1] = 999
+ + " \"items[1]\": 999,\n"
+ // 4. Standalone $IF_NOT_EXISTS - field exists, should use existing value
+ + " \"existingCopy\": {\n" + " \"$IF_NOT_EXISTS\": {\n" + "
\"counter\": 0\n"
+ + " }\n" + " },\n"
+ // 4b. Standalone $IF_NOT_EXISTS - field doesn't exist, should use
fallback
+ + " \"newField\": {\n" + " \"$IF_NOT_EXISTS\": {\n"
+ + " \"nonExistent\": \"fallbackValue\"\n" + " }\n" + "
},\n"
+ // 5. document format: counter = if_not_exists(counter, 0) + 1 = 50 + 1
= 51
+ + " \"counter\": {\n" + " \"$ADD\": [\n"
+ + " {\"$IF_NOT_EXISTS\": {\"counter\": 0}},\n" + " 1\n" +
" ]\n"
+ + " },\n"
+ // 5b. document format with non-existent field: newCounter =
if_not_exists(newCounter, 0) + 10
+ // = 0 + 10 = 10
+ + " \"newCounter\": {\n" + " \"$ADD\": [\n"
+ + " {\"$IF_NOT_EXISTS\": {\"newCounter\": 0}},\n" + "
10\n" + " ]\n"
+ + " },\n"
+ // 5c. document format with 2 simple operands: fieldA = fieldA + 1 = 10
+ 1 = 11
+ + " \"fieldA\": {\n" + " \"$ADD\": [\"fieldA\", 1]\n" + "
},\n"
+ // 5d. document format with nested path: nested.value = nested.value + 1
= 5 + 1 = 6
+ + " \"nested.value\": {\n" + " \"$ADD\": [\"nested.value\",
1]\n" + " },\n"
+ // 5e. document format with top-level dotted key: a.b = a.b + 1 = 7 + 1
= 8
+ + " \"a.b\": {\n" + " \"$ADD\": [\"a.b\", 1]\n" + " }\n" + "
}\n" + "}";
+
+ RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+ UpdateExpressionUtils.updateExpression(expressionDoc, bsonDocument);
+
+ // 1. Verify simple key = value
+ Assert.assertEquals("newValue",
bsonDocument.getString("simpleField").getValue());
+ Assert.assertEquals(42, bsonDocument.getInt32("numericField").getValue());
+
+ // 2. Verify string-based arithmetic
+ Assert.assertEquals(35, bsonDocument.getInt32("sumField").getValue());
+ Assert.assertEquals(15, bsonDocument.getInt32("diffField").getValue());
+
+ // 3. Verify array element set
+ Assert.assertEquals(999,
bsonDocument.getArray("items").get(1).asInt32().getValue());
+ // Other elements unchanged
+ Assert.assertEquals(100,
bsonDocument.getArray("items").get(0).asInt32().getValue());
+ Assert.assertEquals(300,
bsonDocument.getArray("items").get(2).asInt32().getValue());
+
+ // 4. Verify standalone $IF_NOT_EXISTS
+ Assert.assertEquals(50, bsonDocument.getInt32("existingCopy").getValue());
+ Assert.assertEquals("fallbackValue",
bsonDocument.getString("newField").getValue());
+
+ // 5. Verify document format: $ADD with $IF_NOT_EXISTS
+ Assert.assertEquals(51, bsonDocument.getInt32("counter").getValue());
+ Assert.assertEquals(10, bsonDocument.getInt32("newCounter").getValue());
+
+ // 5c. Verify document format: $ADD with 2 simple operands (fieldA =
fieldA + 1)
+ Assert.assertEquals(11, bsonDocument.getInt32("fieldA").getValue());
+
+ // 5d. Verify document format: $ADD with nested path (nested.value =
nested.value + 1)
+ Assert.assertEquals(6,
bsonDocument.getDocument("nested").getInt32("value").getValue());
+
+ // 5e. Verify document format: $ADD with top-level dotted key (a.b = a.b +
1)
+ Assert.assertEquals(8, bsonDocument.getInt32("a.b").getValue());
+
+ // Verify original fields unchanged where expected
+ Assert.assertEquals(25, bsonDocument.getInt32("fieldB").getValue());
+ }
+
}