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 <[email protected]>
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");
+ }
+}