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

vjasani 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 c3a7f470a9 PHOENIX-7692: Path validations for bson update expression 
(#2280)
c3a7f470a9 is described below

commit c3a7f470a94b62d2b4968705c82ee07565c4020f
Author: Palash Chauhan <palashc...@gmail.com>
AuthorDate: Tue Sep 2 15:36:12 2025 -0700

    PHOENIX-7692: Path validations for bson update expression (#2280)
---
 .../bson/BsonUpdateInvalidArgumentException.java   |  32 ++
 .../util/bson/UpdateExpressionUtils.java           |  65 +++-
 .../java/org/apache/phoenix/end2end/Bson4IT.java   |  18 +-
 .../util/bson/UpdateExpressionValidationTest.java  | 417 +++++++++++++++++++++
 4 files changed, 508 insertions(+), 24 deletions(-)

diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/BsonUpdateInvalidArgumentException.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/BsonUpdateInvalidArgumentException.java
new file mode 100644
index 0000000000..b7ef85a725
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/BsonUpdateInvalidArgumentException.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.phoenix.expression.util.bson;
+
+/**
+ * Exception thrown when invalid arguments are provided to BSON update 
expressions.
+ */
+public class BsonUpdateInvalidArgumentException extends 
IllegalArgumentException {
+
+  public BsonUpdateInvalidArgumentException(String message) {
+    super(message);
+  }
+
+  public BsonUpdateInvalidArgumentException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
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 24222c622a..4b3b64b4fb 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
@@ -50,6 +50,9 @@ public class UpdateExpressionUtils {
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(UpdateExpressionUtils.class);
 
+  private static final String INVALID_UPDATE_PATH_MESSAGE =
+    "The document path provided in the update expression is invalid for 
update";
+
   /**
    * Update operator enum values. Any new update operator that requires update 
to deeply nested
    * structures (nested documents or nested arrays), need to have its own type 
added here.
@@ -184,8 +187,8 @@ public class UpdateExpressionUtils {
       bsonDocument.put("$set", new BsonArray(new ArrayList<>(set1)));
       return bsonDocument;
     }
-    throw new RuntimeException("Data type for current value " + currentValue
-      + " is not matching with new value " + setValuesToDelete);
+    // Current value exists but is not a set, or is set of different type
+    throw new BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
   }
 
   /**
@@ -222,6 +225,13 @@ public class UpdateExpressionUtils {
       }
       // If the top level field exists, perform the operation here and return.
       if (topLevelValue != null) {
+        if (
+          !topLevelValue.isNumber() && !topLevelValue.isDecimal128()
+            && !CommonComparisonExpressionUtils.isBsonSet(topLevelValue)
+        ) {
+          // Current value exists but is not a number or set
+          throw new 
BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
+        }
         bsonDocument.put(fieldKey, modifyFieldValueByAdd(topLevelValue, 
newVal));
       } else if (!fieldKey.contains(".") && !fieldKey.contains("[")) {
         bsonDocument.put(fieldKey, newVal);
@@ -265,8 +275,8 @@ public class UpdateExpressionUtils {
       bsonDocument.put("$set", new BsonArray(new ArrayList<>(set1)));
       return bsonDocument;
     }
-    throw new RuntimeException(
-      "Data type for current value " + currentValue + " is not matching with 
new value " + newVal);
+    // Current value exists but is not a number or set, or is set of different 
type
+    throw new BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
   }
 
   /**
@@ -353,7 +363,7 @@ public class UpdateExpressionUtils {
       if (value == null || !value.isDocument()) {
         LOGGER.error("Value is null or not document. Value: {}, Idx: {}, 
fieldKey: {}, New val: {},"
           + " Update op: {}", value, idx, fieldKey, newVal, updateOp);
-        throw new RuntimeException("Value is null or it is not of type 
document.");
+        throw new 
BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
       }
       BsonDocument nestedDocument = (BsonDocument) value;
       curIdx++;
@@ -363,7 +373,7 @@ public class UpdateExpressionUtils {
           BsonValue nestedValue = nestedDocument.get(sb.toString());
           if (nestedValue == null) {
             LOGGER.error("Should have found nested map for {}", sb);
-            return;
+            throw new 
BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
           }
           updateNestedField(nestedValue, curIdx, fieldKey, newVal, updateOp);
           return;
@@ -386,7 +396,7 @@ public class UpdateExpressionUtils {
       if (value == null || !value.isArray()) {
         LOGGER.error("Value is null or not document. Value: {}, Idx: {}, 
fieldKey: {}, New val: {}",
           value, idx, fieldKey, newVal);
-        throw new RuntimeException("Value is null or not array.");
+        throw new 
BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
       }
       BsonArray nestedArray = (BsonArray) value;
       if (curIdx == fieldKey.length()) {
@@ -407,16 +417,16 @@ public class UpdateExpressionUtils {
         if (fieldKey.charAt(i) == '.') {
           BsonValue topFieldValue = ((BsonDocument) value).get(sb.toString());
           if (topFieldValue == null) {
-            LOGGER.error("Incorrect access. Should have found nested 
bsonDocument for {}", sb);
-            throw new RuntimeException("Document does not contain key: " + sb);
+            // Missing parent document - throw exception for all operations
+            throw new 
BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
           }
           updateNestedField(topFieldValue, i, fieldKey, newVal, updateOp);
           return;
         } else if (fieldKey.charAt(i) == '[') {
           BsonValue topFieldValue = ((BsonDocument) value).get(sb.toString());
           if (topFieldValue == null) {
-            LOGGER.error("Incorrect access. Should have found nested list for 
{}", sb);
-            throw new RuntimeException("Document does not contain key: " + sb);
+            // Parent array is missing
+            throw new 
BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
           }
           updateNestedField(topFieldValue, i, fieldKey, newVal, updateOp);
           return;
@@ -445,10 +455,12 @@ public class UpdateExpressionUtils {
     final UpdateOp updateOp, final int arrayIdx, final BsonArray nestedArray) {
     switch (updateOp) {
       case SET: {
-        if (arrayIdx < nestedArray.size()) {
-          nestedArray.set(arrayIdx, newVal);
-        } else {
+        if (arrayIdx >= nestedArray.size()) {
+          // Appending to the end of the array
           nestedArray.add(newVal);
+        } else {
+          // Setting existing element
+          nestedArray.set(arrayIdx, newVal);
         }
         break;
       }
@@ -462,11 +474,21 @@ public class UpdateExpressionUtils {
         if (arrayIdx < nestedArray.size()) {
           BsonValue currentValue = nestedArray.get(arrayIdx);
           if (currentValue != null) {
+            // Validate ADD operation against existing array element
+            if (
+              !currentValue.isNumber() && !currentValue.isDecimal128()
+                && !CommonComparisonExpressionUtils.isBsonSet(currentValue)
+            ) {
+              // Current value exists but is not a number or set
+              throw new 
BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
+            }
             nestedArray.set(arrayIdx, modifyFieldValueByAdd(currentValue, 
newVal));
           } else {
+            // For null array element, just set the value directly
             nestedArray.set(arrayIdx, newVal);
           }
         } else {
+          // For ADD beyond array size, just set the value directly (no 
initialization logic)
           nestedArray.add(newVal);
         }
         break;
@@ -520,9 +542,18 @@ public class UpdateExpressionUtils {
       case ADD: {
         BsonValue currentValue = 
nestedDocument.get(targetNodeFieldKey.toString());
         if (currentValue != null) {
+          // Validate ADD operation against existing value
+          if (
+            !currentValue.isNumber() && !currentValue.isDecimal128()
+              && !CommonComparisonExpressionUtils.isBsonSet(currentValue)
+          ) {
+            // Current value exists but is not a number or set
+            throw new 
BsonUpdateInvalidArgumentException(INVALID_UPDATE_PATH_MESSAGE);
+          }
           nestedDocument.put(targetNodeFieldKey.toString(),
             modifyFieldValueByAdd(currentValue, newVal));
         } else {
+          // For missing field, just set the value directly
           nestedDocument.put(targetNodeFieldKey.toString(), newVal);
         }
         break;
@@ -762,6 +793,12 @@ public class UpdateExpressionUtils {
     }
     BsonArray bsonArray1 = (BsonArray) ((BsonDocument) bsonValue1).get("$set");
     BsonArray bsonArray2 = (BsonArray) ((BsonDocument) bsonValue2).get("$set");
+
+    // Handle empty sets - they are compatible with any set
+    if (bsonArray1.isEmpty() || bsonArray2.isEmpty()) {
+      return true;
+    }
+
     return 
bsonArray1.get(0).getBsonType().equals(bsonArray2.get(0).getBsonType());
   }
 
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson4IT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson4IT.java
index 0c0d8bc471..8d11748ead 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson4IT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson4IT.java
@@ -512,11 +512,8 @@ public class Bson4IT extends ParallelStatsDisabledIT {
     String tableName = generateUniqueName();
     try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
       conn.setAutoCommit(true);
-      conn.createStatement().execute("CREATE TABLE " + tableName + " (" +
-              " hk VARCHAR NOT NULL, " +
-              " sk VARCHAR NOT NULL, " +
-              " col BSON, " +
-              " CONSTRAINT pk PRIMARY KEY (hk, sk))");
+      conn.createStatement().execute("CREATE TABLE " + tableName + " (" + " hk 
VARCHAR NOT NULL, "
+        + " sk VARCHAR NOT NULL, " + " col BSON, " + " CONSTRAINT pk PRIMARY 
KEY (hk, sk))");
 
       RawBsonDocument bsonDoc = RawBsonDocument.parse("{\"a\":1,\"b\":2}");
 
@@ -526,16 +523,17 @@ public class Bson4IT extends ParallelStatsDisabledIT {
       p.setObject(3, bsonDoc);
       p.execute();
 
-      p = conn.prepareStatement("UPSERT INTO " + tableName + " VALUES (?,?)  
ON DUPLICATE KEY UPDATE\n" +
-              " COL = BSON_UPDATE_EXPRESSION(COL,'{}')");
+      p = conn.prepareStatement("UPSERT INTO " + tableName
+        + " VALUES (?,?)  ON DUPLICATE KEY UPDATE\n" + " COL = 
BSON_UPDATE_EXPRESSION(COL,'{}')");
       p.setString(1, "h1");
       p.setString(2, "s1");
-      Pair<Integer, ResultSet> resultPair = 
p.unwrap(PhoenixPreparedStatement.class).executeAtomicUpdateReturnRow();
+      Pair<Integer, ResultSet> resultPair =
+        
p.unwrap(PhoenixPreparedStatement.class).executeAtomicUpdateReturnRow();
       Assert.assertEquals(1, resultPair.getFirst().intValue());
       Assert.assertEquals(bsonDoc, resultPair.getSecond().getObject(3));
 
-      p = conn.prepareStatement("UPSERT INTO " + tableName + " VALUES (?,?)  
ON DUPLICATE KEY UPDATE\n" +
-              " COL = BSON_UPDATE_EXPRESSION(COL,?)");
+      p = conn.prepareStatement("UPSERT INTO " + tableName
+        + " VALUES (?,?)  ON DUPLICATE KEY UPDATE\n" + " COL = 
BSON_UPDATE_EXPRESSION(COL,?)");
       p.setString(1, "h1");
       p.setString(2, "s1");
       p.setObject(3, new BsonDocument());
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionValidationTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionValidationTest.java
new file mode 100644
index 0000000000..9e7c8816e2
--- /dev/null
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/util/bson/UpdateExpressionValidationTest.java
@@ -0,0 +1,417 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.phoenix.util.bson;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import 
org.apache.phoenix.expression.util.bson.BsonUpdateInvalidArgumentException;
+import org.apache.phoenix.expression.util.bson.UpdateExpressionUtils;
+import org.bson.BsonDocument;
+import org.bson.RawBsonDocument;
+import org.junit.Test;
+
+/**
+ * Tests for BSON Update Expression validation logic.
+ */
+public class UpdateExpressionValidationTest {
+
+  // UNSET operations
+
+  @Test
+  public void testUnsetFieldMissing() {
+    // UNSET a: a missing -- No-Op
+    BsonDocument doc = BsonDocument.parse("{}");
+    String updateExpression = "{ \"$UNSET\": { \"a\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{}", doc.toJson());
+  }
+
+  @Test
+  public void testUnsetFieldExists() {
+    // UNSET a: a exists -- Remove
+    BsonDocument doc = BsonDocument.parse("{ \"a\": \"value\" }");
+    String updateExpression = "{ \"$UNSET\": { \"a\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{}", doc.toJson());
+  }
+
+  @Test
+  public void testUnsetNestedParentMissing() {
+    // UNSET a.b: Parent a missing -- Exception
+    BsonDocument doc = BsonDocument.parse("{}");
+    String updateExpression = "{ \"$UNSET\": { \"a.b\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testUnsetParentNotMap() {
+    // UNSET a.b: a exists but not a map -- Exception
+    BsonDocument doc = BsonDocument.parse("{ \"a\": \"notAMap\" }");
+    String updateExpression = "{ \"$UNSET\": { \"a.b\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testUnsetNestedFieldMissing() {
+    // UNSET a.b: a exists but b does not exist -- No-Op
+    BsonDocument doc = BsonDocument.parse("{ \"a\": {} }");
+    String updateExpression = "{ \"$UNSET\": { \"a.b\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": {}}", doc.toJson());
+  }
+
+  @Test
+  public void testUnsetNestedFieldExists() {
+    // UNSET a.b: a exists and b exists -- Remove
+    BsonDocument doc = BsonDocument.parse("{ \"a\": { \"b\": \"value\" } }");
+    String updateExpression = "{ \"$UNSET\": { \"a.b\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": {}}", doc.toJson());
+  }
+
+  @Test
+  public void testUnsetArrayParentMissing() {
+    // UNSET a[i]: a missing -- Exception
+    BsonDocument doc = BsonDocument.parse("{}");
+    String updateExpression = "{ \"$UNSET\": { \"a[0]\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testUnsetParentNotList() {
+    // UNSET a[i]: a exists but not a list -- Exception
+    BsonDocument doc = BsonDocument.parse("{ \"a\": \"notAList\" }");
+    String updateExpression = "{ \"$UNSET\": { \"a[0]\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testUnsetArrayIndexOutOfRange() {
+    // UNSET a[i]: Array index out of range -- No-Op
+    BsonDocument doc = BsonDocument.parse("{ \"a\": [\"item1\"] }");
+    String updateExpression = "{ \"$UNSET\": { \"a[5]\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": [\"item1\"]}", doc.toJson());
+  }
+
+  @Test
+  public void testUnsetArrayIndexValid() {
+    // UNSET a[i]: a exists and index is valid -- Remove
+    BsonDocument doc = BsonDocument.parse("{ \"a\": [\"item1\", \"item2\"] }");
+    String updateExpression = "{ \"$UNSET\": { \"a[0]\": null } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": [\"item2\"]}", doc.toJson());
+  }
+
+  // SET operations
+
+  @Test
+  public void testSetFieldMissing() {
+    // SET a: a missing -- Set
+    BsonDocument doc = BsonDocument.parse("{}");
+    String updateExpression = "{ \"$SET\": { \"a\": \"value\" } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": \"value\"}", doc.toJson());
+  }
+
+  @Test
+  public void testSetNestedParentMissing() {
+    // SET a.b: a missing -- Exception
+    BsonDocument doc = BsonDocument.parse("{}");
+    String updateExpression = "{ \"$SET\": { \"a.b\": \"value\" } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testSetNestedFieldMissing() {
+    // SET a.b: a exists but b missing -- Set
+    BsonDocument doc = BsonDocument.parse("{ \"a\": {} }");
+    String updateExpression = "{ \"$SET\": { \"a.b\": \"value\" } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": {\"b\": \"value\"}}", doc.toJson());
+  }
+
+  @Test
+  public void testSetParentNotMap() {
+    // SET a.b: a exists but not a map -- Exception
+    BsonDocument doc = BsonDocument.parse("{ \"a\": \"notAMap\" }");
+    String updateExpression = "{ \"$SET\": { \"a.b\": \"value\" } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testSetArrayParentMissing() {
+    // SET a[i]: a missing -- Exception
+    BsonDocument doc = BsonDocument.parse("{}");
+    String updateExpression = "{ \"$SET\": { \"a[0]\": \"value\" } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testSetArrayParentNotList() {
+    // SET a[i]: a exists but not a list -- Exception
+    BsonDocument doc = BsonDocument.parse("{ \"a\": \"notAList\" }");
+    String updateExpression = "{ \"$SET\": { \"a[0]\": \"value\" } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testSetArrayIndexBeyondSize1() {
+    // SET a[i]: Index beyond array size -- Append
+    BsonDocument doc = BsonDocument.parse("{ \"a\": [\"item1\"] }");
+    String updateExpression = "{ \"$SET\": { \"a[1]\": \"item2\" } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": [\"item1\", \"item2\"]}", doc.toJson());
+  }
+
+  @Test
+  public void testSetArrayIndexBeyondSize2() {
+    // SET a[i]: Index beyond array size -- Append
+    BsonDocument doc = BsonDocument.parse("{ \"a\": [\"item1\"] }");
+    String updateExpression = "{ \"$SET\": { \"a[6]\": \"item2\" } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": [\"item1\", \"item2\"]}", doc.toJson());
+  }
+
+  @Test
+  public void testSetArrayIndexValid() {
+    // SET a[i]: a exists and index is valid -- Set
+    BsonDocument doc = BsonDocument.parse("{ \"a\": [\"item1\", \"item2\"] }");
+    String updateExpression = "{ \"$SET\": { \"a[0]\": \"newValue\" } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": [\"newValue\", \"item2\"]}", doc.toJson());
+  }
+
+  // ADD operations
+
+  @Test
+  public void testAddFieldNotNumberOrSet() {
+    // ADD a: a exists but not number/set -- Exception
+    BsonDocument doc = BsonDocument.parse("{ \"a\": \"string\" }");
+    String updateExpression = "{ \"$ADD\": { \"a\": 5 } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testAddSetDifferentType() {
+    // ADD a: a is set of different type -- Exception
+    BsonDocument doc = BsonDocument.parse("{ \"a\": { \"$set\": [\"string1\"] 
} }");
+    String updateExpression = "{ \"$ADD\": { \"a\": { \"$set\": [123] } } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testAddNumberMissing() {
+    // ADD a num: a missing -- a=0, Add
+    BsonDocument doc = BsonDocument.parse("{}");
+    String updateExpression = "{ \"$ADD\": { \"a\": 5 } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": 5}", doc.toJson());
+  }
+
+  @Test
+  public void testAddNumberExists() {
+    // ADD a num: a exists -- Add
+    BsonDocument doc = BsonDocument.parse("{ \"a\": 10 }");
+    String updateExpression = "{ \"$ADD\": { \"a\": 5 } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": 15}", doc.toJson());
+  }
+
+  @Test
+  public void testAddSetMissing() {
+    // ADD a set: a missing -- a={}, Add
+    BsonDocument doc = BsonDocument.parse("{}");
+    String updateExpression = "{ \"$ADD\": { \"a\": { \"$set\": [\"item1\"] } 
} }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{\"a\": {\"$set\": [\"item1\"]}}", doc.toJson());
+  }
+
+  @Test
+  public void testAddSetSameType() {
+    // ADD a set: a is set of same type -- Add
+    BsonDocument doc = BsonDocument.parse("{ \"a\": { \"$set\": [\"item1\"] } 
}");
+    String updateExpression = "{ \"$ADD\": { \"a\": { \"$set\": [\"item2\"] } 
} }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    // Note: Order may vary in sets, so we check that both items are present
+    BsonDocument result = doc;
+    String json = result.toJson();
+    assert json.contains("item1");
+    assert json.contains("item2");
+  }
+
+  // DELETE_FROM_SET operations
+
+  @Test
+  public void testDeleteFromSetMissing() {
+    // DELETE_FROM_SET a: a missing -- No-Op
+    BsonDocument doc = BsonDocument.parse("{}");
+    String updateExpression = "{ \"$DELETE_FROM_SET\": { \"a\": { \"$set\": 
[\"item1\"] } } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    assertEquals("{}", doc.toJson());
+  }
+
+  @Test
+  public void testDeleteFromSetNotSet() {
+    // DELETE_FROM_SET a: a exists but not a set -- Exception
+    BsonDocument doc = BsonDocument.parse("{ \"a\": \"string\" }");
+    String updateExpression = "{ \"$DELETE_FROM_SET\": { \"a\": { \"$set\": 
[\"item1\"] } } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testDeleteFromSetDifferentType() {
+    // DELETE_FROM_SET a: a is set of different type -- Exception
+    BsonDocument doc = BsonDocument.parse("{ \"a\": { \"$set\": [\"string1\"] 
} }");
+    String updateExpression = "{ \"$DELETE_FROM_SET\": { \"a\": { \"$set\": 
[123] } } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    try {
+      UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+      fail("Expected BsonUpdateInvalidArgumentException");
+    } catch (BsonUpdateInvalidArgumentException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testDeleteFromSetSameType() {
+    // DELETE_FROM_SET a: a is set of same type -- Delete
+    BsonDocument doc =
+      BsonDocument.parse("{ \"a\": { \"$set\": [\"item1\", \"item2\", 
\"item3\"] } }");
+    String updateExpression = "{ \"$DELETE_FROM_SET\": { \"a\": { \"$set\": 
[\"item2\"] } } }";
+    RawBsonDocument expressionDoc = RawBsonDocument.parse(updateExpression);
+
+    UpdateExpressionUtils.updateExpression(expressionDoc, doc);
+    // Result should contain item1 and item3, but not item2
+    String json = doc.toJson();
+    assert json.contains("item1");
+    assert json.contains("item3");
+    assert !json.contains("item2");
+  }
+}

Reply via email to