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"); + } +}