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

palashc pushed a commit to branch 5.3
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/5.3 by this push:
     new aab3124c61 PHOENIX-7880 Do not treat literal string SET values as 
arithmetic in BSON UpdateExpression (#2496) (#2498)
aab3124c61 is described below

commit aab3124c61112955502e52f71b608de11070a23b
Author: Palash Chauhan <[email protected]>
AuthorDate: Wed Jun 10 11:55:23 2026 -0700

    PHOENIX-7880 Do not treat literal string SET values as arithmetic in BSON 
UpdateExpression (#2496) (#2498)
    
    Co-authored-by: Cursor <[email protected]>
---
 .../util/bson/UpdateExpressionUtils.java           | 109 +++------------------
 .../java/org/apache/phoenix/end2end/Bson2IT.java   |   4 +-
 .../java/org/apache/phoenix/end2end/Bson3IT.java   |  20 +++-
 .../java/org/apache/phoenix/end2end/Bson4IT.java   |  17 +++-
 .../java/org/apache/phoenix/end2end/Bson5IT.java   |   8 +-
 .../util/bson/UpdateExpressionUtilsTest.java       |  58 ++++++++---
 6 files changed, 97 insertions(+), 119 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 09da9d71c7..93efa2a366 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
@@ -18,14 +18,10 @@
 package org.apache.phoenix.expression.util.bson;
 
 import java.math.BigDecimal;
-import java.text.NumberFormat;
-import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import org.bson.BsonArray;
 import org.bson.BsonDecimal128;
 import org.bson.BsonDocument;
@@ -102,7 +98,7 @@ public class UpdateExpressionUtils {
   public static void updateExpression(final BsonDocument updateExpression,
     final BsonDocument bsonDocument) {
 
-    LOGGER.info("Update Expression: {} , current bsonDocument: {}", 
updateExpression, bsonDocument);
+    LOGGER.debug("Update Expression: {} , current bsonDocument: {}", 
updateExpression, bsonDocument);
 
     if (updateExpression.containsKey("$SET")) {
       executeSetExpression((BsonDocument) updateExpression.get("$SET"), 
bsonDocument);
@@ -578,73 +574,26 @@ public class UpdateExpressionUtils {
   }
 
   /**
-   * Retrieve the value to be updated for the given current value. If the 
current value does not
-   * contain any arithmetic operators, the current value is returned without 
any modifications. If
-   * the current value contains arithmetic expressions like "a + b" or "a - 
b", the values of
-   * operands are retrieved from the given document and if the values are 
numeric, the given
-   * 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. 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. If the current value is a bson 
document with $LIST_APPEND
-   * as key, the value is a two-element array of operands; each operand 
resolves to a BsonArray
-   * (literal array, a path string referring to an existing array attribute, 
or an $IF_NOT_EXISTS
-   * document whose resolved value is an array) and the two arrays are 
concatenated in order with
-   * duplicates preserved.
+   * Retrieve the value to be updated for the given current value. Arithmetic 
and other computed SET
+   * values are always carried as explicit BSON documents (never inferred from 
the textual content
+   * of a string value), so any non-document value - including a {@link 
BsonString} that happens to
+   * contain " + " or " - " - is treated as a literal and returned without 
modification. 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. 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. If the
+   * current value is a bson document with $LIST_APPEND as key, the value is a 
two-element array of
+   * operands; each operand resolves to a BsonArray (literal array, a path 
string referring to an
+   * existing array attribute, or an $IF_NOT_EXISTS document whose resolved 
value is an array) and
+   * the two arrays are concatenated in order with duplicates preserved.
    * @param curValue     The current value.
    * @param bsonDocument The document with all field key-value pairs.
    * @return Updated values to be used by SET operation.
    */
   private static BsonValue getNewFieldValue(final BsonValue curValue,
     final BsonDocument bsonDocument) {
-    if (
-      curValue != null && curValue.isString()
-        && (((BsonString) curValue).getValue().contains(" + ")
-          || ((BsonString) curValue).getValue().contains(" - "))
-    ) {
-      String[] tokens = ((BsonString) curValue).getValue().split("\\s+");
-      boolean addNum = true;
-      // Pattern pattern = Pattern.compile(":?[a-zA-Z0-9]+");
-      Pattern pattern = Pattern.compile("[#:$]?[^\\s\\n]+");
-      Number newNum = null;
-      for (String token : tokens) {
-        if (token.equals("+")) {
-          addNum = true;
-          continue;
-        } else if (token.equals("-")) {
-          addNum = false;
-          continue;
-        }
-        Matcher matcher = pattern.matcher(token);
-        if (matcher.find()) {
-          String operand = matcher.group();
-          Number literalNum;
-          BsonValue topLevelValue = bsonDocument.get(operand);
-          BsonValue bsonValue = topLevelValue != null
-            ? topLevelValue
-            : CommonComparisonExpressionUtils.getFieldFromDocument(operand, 
bsonDocument);
-
-          if (bsonValue == null && (literalNum = stringToNumber(operand)) != 
null) {
-            Number val = literalNum;
-            newNum =
-              newNum == null ? val : (addNum ? addNum(newNum, val) : 
subtractNum(newNum, val));
-          } else {
-            if (bsonValue == null) {
-              throw new IllegalArgumentException("Operand " + operand + " does 
not exist");
-            }
-            if (!bsonValue.isNumber() && !bsonValue.isDecimal128()) {
-              throw new IllegalArgumentException(
-                "Operand " + operand + " is not provided as number type");
-            }
-            Number val = getNumberFromBsonNumber((BsonNumber) bsonValue);
-            newNum =
-              newNum == null ? val : (addNum ? addNum(newNum, val) : 
subtractNum(newNum, val));
-          }
-        }
-      }
-      return getBsonNumberFromNumber(newNum);
-    } else if (curValue instanceof BsonDocument) {
+    if (curValue instanceof BsonDocument) {
       BsonDocument doc = (BsonDocument) curValue;
       if (doc.get("$IF_NOT_EXISTS") != null) {
         return resolveIfNotExists(doc, bsonDocument);
@@ -823,34 +772,6 @@ public class UpdateExpressionUtils {
     throw new RuntimeException("Number type is not known for number: " + 
number);
   }
 
-  /**
-   * Convert the given String to Number.
-   * @param number The String represented numeric value.
-   * @return The Number object.
-   */
-  private static Number stringToNumber(String number) {
-    try {
-      return Integer.parseInt(number);
-    } catch (NumberFormatException e) {
-      // no-op
-    }
-    try {
-      return Long.parseLong(number);
-    } catch (NumberFormatException e) {
-      // no-op
-    }
-    try {
-      return Double.parseDouble(number);
-    } catch (NumberFormatException e) {
-      // no-op
-    }
-    try {
-      return NumberFormat.getInstance().parse(number);
-    } catch (ParseException e) {
-      return null;
-    }
-  }
-
   /**
    * Convert Number to BsonNumber.
    * @param number The Number object.
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson2IT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson2IT.java
index 73b6b21fda..855795627d 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson2IT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson2IT.java
@@ -258,9 +258,9 @@ public class Bson2IT extends ParallelStatsDisabledIT {
        */
 
       updateExp = "{\n" + "  \"$SET\": {\n" + "    \"Id1\": \"12345\",\n"
-        + "    \"NestedList1[0]\": \"NestedList1[0] + 12.22\",\n"
+        + "    \"NestedList1[0]\": { \"$ADD\": [ \"NestedList1[0]\", 12.22 ] 
},\n"
         + "    \"NestedList1[3]\": null,\n" + "    \"NestedList1[4]\": true,\n"
-        + "    \"attr_5[0]\": \"attr_5[0] - 10\"\n" + "  }\n" + "}";
+        + "    \"attr_5[0]\": { \"$SUBTRACT\": [ \"attr_5[0]\", 10 ] }\n" + "  
}\n" + "}";
 
       stmt = conn.prepareStatement("UPSERT INTO " + tableName
         + " VALUES (?,?) ON DUPLICATE KEY UPDATE COL = 
BSON_UPDATE_EXPRESSION(COL, '" + updateExp
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java
index a0ccdb3765..1f78df07c0 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java
@@ -232,7 +232,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 529.435")))
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new 
BsonNull()));
 
@@ -764,7 +766,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 529.435")))
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new 
BsonNull()));
 
@@ -1163,7 +1167,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 529.435")))
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new 
BsonNull()));
 
@@ -1536,7 +1542,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 529.435")))
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new 
BsonNull()));
 
@@ -1951,7 +1959,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 529.435")))
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new 
BsonNull()));
 
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 f35cc50f5c..f7f226e1c4 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
@@ -60,6 +60,7 @@ import org.bson.BsonArray;
 import org.bson.BsonBinary;
 import org.bson.BsonDocument;
 import org.bson.BsonDouble;
+import org.bson.BsonInt32;
 import org.bson.BsonNull;
 import org.bson.BsonString;
 import org.bson.Document;
@@ -641,7 +642,9 @@ public class Bson4IT extends ParallelStatsDisabledIT {
           .append("browserling", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 529.435")))
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new 
BsonNull()));
 
@@ -725,7 +728,9 @@ public class Bson4IT extends ParallelStatsDisabledIT {
           .append("new_field1", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft_new_val"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 123")));
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonInt32(123))))));
 
       stmt = conn.prepareStatement(
         "UPSERT INTO " + tableName + " VALUES (?) ON DUPLICATE KEY UPDATE COL 
= CASE WHEN"
@@ -827,7 +832,9 @@ public class Bson4IT extends ParallelStatsDisabledIT {
           .append("browserling", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 529.435")))
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new 
BsonNull()));
 
@@ -907,7 +914,9 @@ public class Bson4IT extends ParallelStatsDisabledIT {
           .append("new_field1", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft_new_val"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 123")));
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonInt32(123))))));
 
       stmt = conn.prepareStatement(
         "UPSERT INTO " + tableName + " VALUES (?) ON DUPLICATE KEY UPDATE_ONLY 
COL = CASE WHEN"
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson5IT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson5IT.java
index 3e6a4f1989..9d1b477d17 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson5IT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson5IT.java
@@ -252,7 +252,9 @@ public class Bson5IT extends ParallelStatsDisabledIT {
           .append("browserling", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 529.435")))
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new 
BsonNull()));
 
@@ -487,7 +489,9 @@ public class Bson5IT extends ParallelStatsDisabledIT {
           .append("browserling", new 
BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new 
BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonString("track[0].shot[2][0].city.problem[2] + 529.435")))
+            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
+              new BsonString("track[0].shot[2][0].city.problem[2]"),
+              new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new 
BsonNull()));
 
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 db6dc05481..eff0c8a9c9 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
@@ -21,6 +21,7 @@ import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.phoenix.expression.util.bson.UpdateExpressionUtils;
 import org.bson.BsonBinaryReader;
 import org.bson.BsonDocument;
+import org.bson.BsonString;
 import org.bson.RawBsonDocument;
 import org.bson.codecs.BsonDocumentCodec;
 import org.bson.codecs.DecoderContext;
@@ -854,7 +855,7 @@ public class UpdateExpressionUtilsTest {
       + "  \"InPublication\" : false,\n" + "  \"ColorBytes\" : {\n" + "    
\"$binary\" : {\n"
       + "      \"base64\" : \"QmxhY2s=\",\n" + "      \"subType\" : \"00\"\n" 
+ "    }\n" + "  },\n"
       + "  \"ISBN\" : \"111-1111111111\",\n"
-      + "  \"NestedList1\" : [ -473.11999999999995, \"1234abcd\", [ 
\"xyz0123\", {\n"
+      + "  \"NestedList1\" : [ \"NestedList1[0] + 12.22\", \"1234abcd\", [ 
\"xyz0123\", {\n"
       + "    \"InPublication\" : false,\n" + "    \"BinaryTitleSet\" : {\n"
       + "      \"$set\" : [ {\n" + "        \"$binary\" : {\n"
       + "          \"base64\" : \"Qm9vayAxMDExIFRpdGxlIEJpbmFyeQ==\",\n"
@@ -888,10 +889,10 @@ public class UpdateExpressionUtilsTest {
       + "        \"base64\" : \"MjA0OHU1bmJsd2plaVdGR1RIKDRiZjkzMA==\",\n"
       + "        \"subType\" : \"00\"\n" + "      }\n" + "    },\n" + "    
\"n_attr_3\" : true,\n"
       + "    \"n_attr_4\" : null,\n" + "    \"n_attr_10\" : true,\n"
-      + "    \"n_attr_20\" : \"str_val_0\"\n" + "  },\n" + "  \"attr_5\" : [ 
1224, \"str001\", {\n"
-      + "    \"$binary\" : {\n" + "      \"base64\" : \"AAECAwQF\",\n"
-      + "      \"subType\" : \"00\"\n" + "    }\n" + "  } ],\n"
-      + "  \"NestedList12\" : [ -485.34, \"1234abcd\", [ {\n"
+      + "    \"n_attr_20\" : \"str_val_0\"\n" + "  },\n"
+      + "  \"attr_5\" : [ \"attr_5[0] - 10\", \"str001\", {\n" + "    
\"$binary\" : {\n"
+      + "      \"base64\" : \"AAECAwQF\",\n" + "      \"subType\" : \"00\"\n" 
+ "    }\n"
+      + "  } ],\n" + "  \"NestedList12\" : [ -485.34, \"1234abcd\", [ {\n"
       + "    \"$set\" : [ \"xyz01234\", \"xyz0123\", \"abc01234\" ]\n" + "  }, 
{\n"
       + "    \"$set\" : [ {\n" + "      \"$binary\" : {\n" + "        
\"base64\" : \"dmFsMDE=\",\n"
       + "        \"subType\" : \"00\"\n" + "      }\n" + "    }, {\n" + "      
\"$binary\" : {\n"
@@ -942,7 +943,7 @@ public class UpdateExpressionUtilsTest {
     // }
     // },
     // "ISBN" : "111-1111111111",
-    // "NestedList1" : [ -473.11999999999995, "1234abcd", [ "xyz0123", {
+    // "NestedList1" : [ "NestedList1[0] + 12.22", "1234abcd", [ "xyz0123", {
     // "InPublication" : false,
     // "BinaryTitleSet" : {
     // "$set" : [ {
@@ -1016,7 +1017,7 @@ public class UpdateExpressionUtilsTest {
     // "n_attr_10" : true,
     // "n_attr_20" : "str_val_0"
     // },
-    // "attr_5" : [ 1224, "str001", {
+    // "attr_5" : [ "attr_5[0] - 10", "str001", {
     // "$binary" : {
     // "base64" : "AAECAwQF",
     // "subType" : "00"
@@ -1198,9 +1199,10 @@ public class UpdateExpressionUtilsTest {
     String updateExpression = "{\n" + "  \"$SET\": {\n"
     // 1. Simple key = value
       + "    \"simpleField\": \"newValue\",\n" + "    \"numericField\": 42,\n"
-      // 2. string-based arithmetic: fieldA + fieldB = 10 + 25 = 35
+      // 2. literal string containing " + " is stored verbatim (NOT 
interpreted as arithmetic).
+      // Arithmetic is only performed via the explicit $ADD / $SUBTRACT 
document forms below.
       + "    \"sumField\": \"fieldA + fieldB\",\n"
-      // 2b. string-based arithmetic with subtraction: fieldB - fieldA = 25 - 
10 = 15
+      // 2b. literal string containing " - " is stored verbatim (NOT 
interpreted as arithmetic).
       + "    \"diffField\": \"fieldB - fieldA\",\n"
       // 3. Array element set: items[1] = 999
       + "    \"items[1]\": 999,\n"
@@ -1233,9 +1235,10 @@ public class UpdateExpressionUtilsTest {
     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());
+    // 2. Verify literal strings containing " + " / " - " are stored verbatim 
and are never
+    // mistaken for arithmetic expressions.
+    Assert.assertEquals("fieldA + fieldB", 
bsonDocument.getString("sumField").getValue());
+    Assert.assertEquals("fieldB - fieldA", 
bsonDocument.getString("diffField").getValue());
 
     // 3. Verify array element set
     Assert.assertEquals(999, 
bsonDocument.getArray("items").get(1).asInt32().getValue());
@@ -1264,6 +1267,37 @@ public class UpdateExpressionUtilsTest {
     Assert.assertEquals(25, bsonDocument.getInt32("fieldB").getValue());
   }
 
+  /**
+   * Regression test for literal string SET values that contain " + " or " - 
". Such values must be
+   * stored verbatim and must never be mistaken for arithmetic expressions 
(which previously threw
+   * "Operand ... does not exist"). Arithmetic is only performed via the 
explicit $ADD / $SUBTRACT
+   * document forms.
+   */
+  @Test
+  public void testLiteralStringWithArithmeticOperatorsIsStoredVerbatim() {
+    BsonDocument bsonDocument = new BsonDocument();
+
+    // Literal JSON string containing " - ".
+    String propertiesLiteral = "{\"teamId\":\"abc123\"," + "\"teamName\":\"Foo 
- Bar Baz Service\","
+      + "\"channelName\":\"#foo-notifications\"}";
+
+    BsonDocument setDoc = new BsonDocument();
+    setDoc.put("properties", new BsonString(propertiesLiteral));
+    // Literal string containing " + ".
+    setDoc.put("equation", new BsonString("E = mc^2 + offset"));
+    // Plain literal that looks like a subtraction of two field names.
+    setDoc.put("company", new BsonString("Acme - Widgets"));
+
+    BsonDocument updateExpression = new BsonDocument();
+    updateExpression.put("$SET", setDoc);
+
+    UpdateExpressionUtils.updateExpression(updateExpression, bsonDocument);
+
+    Assert.assertEquals(propertiesLiteral, 
bsonDocument.getString("properties").getValue());
+    Assert.assertEquals("E = mc^2 + offset", 
bsonDocument.getString("equation").getValue());
+    Assert.assertEquals("Acme - Widgets", 
bsonDocument.getString("company").getValue());
+  }
+
   private static BsonDocument seedListAppendDoc() {
     return BsonDocument
       .parse("{" + "\"events\": [\"a\", \"b\"]," + "\"numeric\": 42," + 
"\"text\": \"hello\","

Reply via email to