diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/DocumentExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/DocumentExpression.java index cf1522d1623..833d9546040 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/DocumentExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/DocumentExpression.java @@ -16,10 +16,83 @@ package com.mongodb.client.model.expressions; +import org.bson.conversions.Bson; +import org.bson.types.Decimal128; + +import java.time.Instant; + +import static com.mongodb.client.model.expressions.Expressions.of; + /** * Expresses a document value. A document is an ordered set of fields, where the * key is a string value, mapping to a value of any other expression type. */ public interface DocumentExpression extends Expression { + DocumentExpression setField(String fieldName, Expression exp); + + DocumentExpression unsetField(String fieldName); + + Expression getField(String fieldName); + + BooleanExpression getBoolean(String fieldName); + + BooleanExpression getBoolean(String fieldName, BooleanExpression other); + + default BooleanExpression getBoolean(final String fieldName, final boolean other) { + return getBoolean(fieldName, of(other)); + } + + NumberExpression getNumber(String fieldName); + + NumberExpression getNumber(String fieldName, NumberExpression other); + + default NumberExpression getNumber(final String fieldName, final double other) { + return getNumber(fieldName, of(other)); + } + + default NumberExpression getNumber(final String fieldName, final Decimal128 other) { + return getNumber(fieldName, of(other)); + } + + IntegerExpression getInteger(String fieldName); + + IntegerExpression getInteger(String fieldName, IntegerExpression other); + + default IntegerExpression getInteger(final String fieldName, final int other) { + return getInteger(fieldName, of(other)); + } + + default IntegerExpression getInteger(final String fieldName, final long other) { + return getInteger(fieldName, of(other)); + } + + + StringExpression getString(String fieldName); + + StringExpression getString(String fieldName, StringExpression other); + + default StringExpression getString(final String fieldName, final String other) { + return getString(fieldName, of(other)); + } + + DateExpression getDate(String fieldName); + DateExpression getDate(String fieldName, DateExpression other); + + default DateExpression getDate(final String fieldName, final Instant other) { + return getDate(fieldName, of(other)); + } + + DocumentExpression getDocument(String fieldName); + DocumentExpression getDocument(String fieldName, DocumentExpression other); + + default DocumentExpression getDocument(final String fieldName, final Bson other) { + return getDocument(fieldName, of(other)); + } + + ArrayExpression getArray(String fieldName); + + ArrayExpression getArray(String fieldName, ArrayExpression other); + + DocumentExpression merge(DocumentExpression other); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/Expression.java b/driver-core/src/main/com/mongodb/client/model/expressions/Expression.java index 1ef5e6460ef..f203175c60b 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/Expression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/Expression.java @@ -100,14 +100,16 @@ public interface Expression { /** * also checks for nulls - * @param or + * @param other * @return */ - BooleanExpression isBooleanOr(BooleanExpression or); - NumberExpression isNumberOr(NumberExpression or); - StringExpression isStringOr(StringExpression or); - DateExpression isDateOr(DateExpression or); - ArrayExpression isArrayOr(ArrayExpression or); - T isDocumentOr(T or); + BooleanExpression isBooleanOr(BooleanExpression other); + NumberExpression isNumberOr(NumberExpression other); + IntegerExpression isIntegerOr(IntegerExpression other); + StringExpression isStringOr(StringExpression other); + DateExpression isDateOr(DateExpression other); + ArrayExpression isArrayOr(ArrayExpression other); + T isDocumentOr(T other); + StringExpression asString(); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java index e67044a7315..7629def2c15 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java @@ -137,6 +137,113 @@ public R cond(final R left, final R right) { return newMqlExpression(ast("$cond", left, right)); } + /** @see DocumentExpression */ + + private Function getFieldInternal(final String fieldName) { + return (cr) -> { + BsonValue value = fieldName.startsWith("$") + ? new BsonDocument("$literal", new BsonString(fieldName)) + : new BsonString(fieldName); + return astDoc("$getField", new BsonDocument() + .append("input", this.fn.apply(cr).bsonValue) + .append("field", value)); + }; + } + + @Override + public Expression getField(final String fieldName) { + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public BooleanExpression getBoolean(final String fieldName) { + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public BooleanExpression getBoolean(final String fieldName, final BooleanExpression other) { + return getBoolean(fieldName).isBooleanOr(other); + } + + @Override + public NumberExpression getNumber(final String fieldName) { + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public NumberExpression getNumber(final String fieldName, final NumberExpression other) { + return getNumber(fieldName).isNumberOr(other); + } + + @Override + public IntegerExpression getInteger(final String fieldName) { + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public IntegerExpression getInteger(final String fieldName, final IntegerExpression other) { + return getInteger(fieldName).isIntegerOr(other); + } + + @Override + public StringExpression getString(final String fieldName) { + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public StringExpression getString(final String fieldName, final StringExpression other) { + return getString(fieldName).isStringOr(other); + } + + @Override + public DateExpression getDate(final String fieldName) { + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public DateExpression getDate(final String fieldName, final DateExpression other) { + return getDate(fieldName).isDateOr(other); + } + + @Override + public DocumentExpression getDocument(final String fieldName) { + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public DocumentExpression getDocument(final String fieldName, final DocumentExpression other) { + return getDocument(fieldName).isDocumentOr(other); + } + + @Override + public ArrayExpression getArray(final String fieldName) { + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public ArrayExpression getArray(final String fieldName, final ArrayExpression other) { + return getArray(fieldName).isArrayOr(other); + } + + @Override + public DocumentExpression merge(final DocumentExpression other) { + return new MqlExpression<>(ast("$mergeObjects", other)); + } + + @Override + public DocumentExpression setField(final String fieldName, final Expression exp) { + return newMqlExpression((cr) -> astDoc("$setField", new BsonDocument() + .append("field", new BsonString(fieldName)) + .append("input", this.toBsonValue(cr)) + .append("value", extractBsonValue(cr, exp)))); + } + + @Override + public DocumentExpression unsetField(final String fieldName) { + return newMqlExpression((cr) -> astDoc("$unsetField", new BsonDocument() + .append("field", new BsonString(fieldName)) + .append("input", this.toBsonValue(cr)))); + } /** @see Expression */ @@ -175,8 +282,8 @@ public BooleanExpression isBoolean() { } @Override - public BooleanExpression isBooleanOr(final BooleanExpression or) { - return this.isBoolean().cond(this, or); + public BooleanExpression isBooleanOr(final BooleanExpression other) { + return this.isBoolean().cond(this, other); } public BooleanExpression isNumber() { @@ -184,8 +291,17 @@ public BooleanExpression isNumber() { } @Override - public NumberExpression isNumberOr(final NumberExpression or) { - return this.isNumber().cond(this, or); + public NumberExpression isNumberOr(final NumberExpression other) { + return this.isNumber().cond(this, other); + } + + public BooleanExpression isInteger() { + return this.isNumber().cond(this.eq(this.round()), of(false)); + } + + @Override + public IntegerExpression isIntegerOr(final IntegerExpression other) { + return this.isInteger().cond(this, other); } public BooleanExpression isString() { @@ -193,8 +309,8 @@ public BooleanExpression isString() { } @Override - public StringExpression isStringOr(final StringExpression or) { - return this.isString().cond(this, or); + public StringExpression isStringOr(final StringExpression other) { + return this.isString().cond(this, other); } public BooleanExpression isDate() { @@ -202,19 +318,26 @@ public BooleanExpression isDate() { } @Override - public DateExpression isDateOr(final DateExpression or) { - return this.isDate().cond(this, or); + public DateExpression isDateOr(final DateExpression other) { + return this.isDate().cond(this, other); } public BooleanExpression isArray() { return new MqlExpression<>(astWrapped("$isArray")); } - @SuppressWarnings("unchecked") // TODO + /** + * checks if array (but cannot check type) + * user asserts array is of type R + * + * @param other + * @return + * @param + */ + @SuppressWarnings("unchecked") @Override - public ArrayExpression isArrayOr(final ArrayExpression or) { - // TODO it seems that ArrEx does not make sense here - return (ArrayExpression) this.isArray().cond(this.assertImplementsAllExpressions(), or); + public ArrayExpression isArrayOr(final ArrayExpression other) { + return (ArrayExpression) this.isArray().cond(this.assertImplementsAllExpressions(), other); } public BooleanExpression isDocument() { @@ -222,8 +345,8 @@ public BooleanExpression isDocument() { } @Override - public R isDocumentOr(final R or) { - return this.isDocument().cond(this.assertImplementsAllExpressions(), or); + public R isDocumentOr(final R other) { + return this.isDocument().cond(this.assertImplementsAllExpressions(), other); } @Override diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/AbstractExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/AbstractExpressionsFunctionalTest.java index 9a4899a0f76..a167120f041 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/AbstractExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/AbstractExpressionsFunctionalTest.java @@ -40,6 +40,7 @@ import static com.mongodb.ClusterFixture.serverVersionAtLeast; import static com.mongodb.client.model.Aggregates.addFields; import static org.bson.codecs.configuration.CodecRegistries.fromProviders; +import static org.bson.conversions.Bson.DEFAULT_CODEC_REGISTRY; import static org.junit.jupiter.api.Assertions.assertEquals; public abstract class AbstractExpressionsFunctionalTest extends OperationTest { @@ -70,7 +71,8 @@ protected void assertExpression(@Nullable final Object expected, final Expressio return; } - BsonValue expressionValue = ((MqlExpression) expression).toBsonValue(fromProviders(new BsonValueCodecProvider())); + BsonValue expressionValue = ((MqlExpression) expression).toBsonValue( + fromProviders(new BsonValueCodecProvider(), DEFAULT_CODEC_REGISTRY)); BsonValue bsonValue = new BsonDocumentFragmentCodec().readValue( new JsonReader(expectedMql), DecoderContext.builder().build()); diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArrayExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArrayExpressionsFunctionalTest.java index 24145443c23..69f3399da02 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArrayExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArrayExpressionsFunctionalTest.java @@ -17,6 +17,7 @@ package com.mongodb.client.model.expressions; import com.mongodb.MongoCommandException; +import org.bson.Document; import org.bson.types.Decimal128; import org.junit.jupiter.api.Test; @@ -79,13 +80,21 @@ public void literalsTest() { Arrays.asList(Instant.parse("2007-12-03T10:15:30.00Z")), ofDateArray(Instant.parse("2007-12-03T10:15:30.00Z")), "[{'$date': '2007-12-03T10:15:30.00Z'}]"); + // Document - // ... + ArrayExpression documentArray = ofArray( + of(Document.parse("{a: 1}")), + of(Document.parse("{b: 2}"))); + assertExpression( + Arrays.asList(Document.parse("{a: 1}"), Document.parse("{b: 2}")), + documentArray, + "[{'$literal': {'a': 1}}, {'$literal': {'b': 2}}]"); // Array - ArrayExpression> arrays = ofArray(ofArray(), ofArray()); + ArrayExpression> arrayArray = ofArray(ofArray(), ofArray()); assertExpression( - Arrays.asList(Collections.emptyList(), Collections.emptyList()), arrays, + Arrays.asList(Collections.emptyList(), Collections.emptyList()), + arrayArray, "[[], []]"); // Mixed @@ -176,9 +185,10 @@ public void elementAtTest() { assertExpression( Arrays.asList(1, 2, 3).get(0), // 0.0 is a valid integer value - array123.elementAt((IntegerExpression) of(0.0)), + array123.elementAt(of(0.0).isIntegerOr(of(-1))), // MQL: - "{'$arrayElemAt': [[1, 2, 3], 0.0]}"); + "{'$arrayElemAt': [[1, 2, 3], {'$cond': [{'$cond': " + + "[{'$isNumber': [0.0]}, {'$eq': [0.0, {'$round': 0.0}]}, false]}, 0.0, -1]}]}"); // negatives assertExpression( Arrays.asList(1, 2, 3).get(3 - 1), diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ComparisonExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ComparisonExpressionsFunctionalTest.java index 0587a6d7871..ef68da2d394 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ComparisonExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ComparisonExpressionsFunctionalTest.java @@ -36,7 +36,7 @@ class ComparisonExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#comparison-expression-operators // (Complete as of 6.0) - // Comparison expressions are part of the the generic Expression class. + // Comparison expressions are part of the generic Expression class. // https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order/#std-label-bson-types-comparison-order private final List sampleValues = Arrays.asList( diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/DocumentExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/DocumentExpressionsFunctionalTest.java new file mode 100644 index 00000000000..a00a16f5d27 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/DocumentExpressionsFunctionalTest.java @@ -0,0 +1,250 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed 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 com.mongodb.client.model.expressions; + +import org.bson.BsonDocument; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.bson.types.Decimal128; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; + +import static com.mongodb.client.model.expressions.Expressions.of; +import static com.mongodb.client.model.expressions.Expressions.ofIntegerArray; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SuppressWarnings("ConstantConditions") +class DocumentExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#object-expression-operators + // (Complete as of 6.0) + + private static DocumentExpression ofDoc(final String ofDoc) { + return of(BsonDocument.parse(ofDoc)); + } + + private final DocumentExpression a1 = ofDoc("{a: 1}"); + private final DocumentExpression ax1ay2 = ofDoc("{a: {x: 1, y: 2}}"); + + @Test + public void literalsTest() { + assertExpression( + BsonDocument.parse("{'a': 1}"), + ofDoc("{a: 1}"), + "{'$literal': {'a': 1}}"); + assertThrows(IllegalArgumentException.class, () -> of((Bson) null)); + // doc inside doc + assertExpression( + BsonDocument.parse("{'a': {'x': 1, 'y': 2}}"), + ofDoc("{a: {x: 1, y: 2}}"), + "{'$literal': {'a': {'x': 1, 'y': 2}}}"); + // empty + assertExpression( + BsonDocument.parse("{}"), + ofDoc("{}"), + "{'$literal': {}}"); + // ensure is literal + assertExpression(BsonDocument.parse( + "{'lit': {'$not': true}}"), + of(BsonDocument.parse("{lit: {'$not': true} }")), + "{'$literal': {'lit': {'$not': true}}}"); + } + + @Test + public void getFieldTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/getField/ (100) + // these count as assertions by the user that the value is of the correct type + + assertExpression(1, + a1.getField("a"), + "{'$getField': {'input': {'$literal': {'a': 1}}, 'field': 'a'}}"); + assertExpression(2, + a1.getInteger("a").multiply(2), + "{'$multiply': [{'$getField': {'input': {'$literal': {'a': 1}}, 'field': 'a'}}, 2]}"); + + // different types + String getFieldMql = "{'$getField': {'input': {'$literal': {'a': 1}}, 'field': 'a'}}"; + assertExpression(1, a1.getNumber("a"), getFieldMql); + // these are all violations, since they assert the wrong type, but we are testing the generated Mql: + assertExpression(1, a1.getBoolean("a"), getFieldMql); + assertExpression(1, a1.getInteger("a"), getFieldMql); + assertExpression(1, a1.getString("a"), getFieldMql); + assertExpression(1, a1.getDate("a"), getFieldMql); + assertExpression(1, a1.getArray("a"), getFieldMql); + assertExpression(1, a1.getDocument("a"), getFieldMql); + // usage with other expressions + assertExpression(false, ofDoc("{a: true}").getBoolean("a").not()); + assertExpression(0.5, ofDoc("{a: 1.0}").getNumber("a").divide(2)); + assertExpression(8, ofIntegerArray(9, 8, 7).elementAt(ofDoc("{a: 1.0}").getInteger("a"))); + assertExpression("a", ofDoc("{a: 'A'}").getString("a").toLower()); + assertExpression(12, ofDoc("{a: {'$date': '2007-12-03T10:15:30.005Z'}}") + .getDate("a").month(of("UTC"))); + assertExpression(3, ofDoc("{a: [3, 2]}").getArray("a").first()); + assertExpression(2, ofDoc("{a: {b: 2}}").getDocument("a").getInteger("b")); + + // field names, not paths + DocumentExpression doc = ofDoc("{a: {b: 2}, 'a.b': 3, 'a$b': 4, '$a.b': 5}"); + assertExpression(2, doc.getDocument("a").getInteger("b")); + assertExpression(3, doc.getInteger("a.b")); + assertExpression(4, doc.getInteger("a$b")); + assertExpression(5, + doc.getInteger("$a.b"), + "{'$getField': {'input': {'$literal': {'a': {'b': 2}, 'a.b': 3, 'a$b': 4, '$a.b': 5}}, " + + "'field': {'$literal': '$a.b'}}}"); + } + + @Test + public void getFieldOrTest() { + // convenience + assertExpression(true, ofDoc("{a: true}").getBoolean("a", false)); + assertExpression(1.0, ofDoc("{a: 1.0}").getNumber("a", 99)); + assertExpression(1.0, ofDoc("{a: 1.0}").getNumber("a", Decimal128.parse("99"))); + assertExpression("A", ofDoc("{a: 'A'}").getString("a", "Z")); + assertExpression(2007, ofDoc("{a: {'$date': '2007-12-03T10:15:30.005Z'}}") + .getDate("a", Instant.EPOCH).year(of("UTC"))); + // no convenience for arrays + assertExpression(Document.parse("{b: 2}"), ofDoc("{a: {b: 2}}") + .getDocument("a", Document.parse("{z: 99}"))); + + // normal + assertExpression(true, ofDoc("{a: true}").getBoolean("a", of(false))); + assertExpression(1.0, ofDoc("{a: 1.0}").getNumber("a", of(99))); + assertExpression(1.0, ofDoc("{a: 1.0}").getInteger("a", of(99))); + assertExpression("A", ofDoc("{a: 'A'}").getString("a", of("Z"))); + assertExpression(2007, ofDoc("{a: {'$date': '2007-12-03T10:15:30.005Z'}}") + .getDate("a", of(Instant.EPOCH)).year(of("UTC"))); + assertExpression(Arrays.asList(3, 2), ofDoc("{a: [3, 2]}").getArray("a", ofIntegerArray(99, 88))); + assertExpression(Document.parse("{b: 2}"), ofDoc("{a: {b: 2}}") + .getDocument("a", of(Document.parse("{z: 99}")))); + + // right branch (missing field) + assertExpression(false, ofDoc("{}").getBoolean("a", false)); + assertExpression(99, ofDoc("{}").getInteger("a", 99)); + assertExpression(99, ofDoc("{}").getNumber("a", 99)); + assertExpression(99L, ofDoc("{}").getNumber("a", 99L)); + assertExpression(99.0, ofDoc("{}").getNumber("a", 99.0)); + assertExpression(Decimal128.parse("99"), ofDoc("{}").getNumber("a", Decimal128.parse("99"))); + assertExpression("Z", ofDoc("{}").getString("a", "Z")); + assertExpression(1970, ofDoc("{}") + .getDate("a", Instant.EPOCH).year(of("UTC"))); + assertExpression(Arrays.asList(99, 88), ofDoc("{}").getArray("a", ofIntegerArray(99, 88))); + assertExpression(Document.parse("{z: 99}"), ofDoc("{}") + .getDocument("a", Document.parse("{z: 99}"))); + + // int vs num + assertExpression(99, ofDoc("{a: 1.1}").getInteger("a", of(99))); + } + + @Test + public void getFieldMissingTest() { + // missing fields + assertExpression( + BsonDocument.parse("{'a': 1}"), + a1.setField("z", a1.getBoolean("missing"))); + assertExpression( + BsonDocument.parse("{'a': 1}"), + a1.setField("z", a1.getDocument("missing").getDocument("also_missing"))); + assertExpression( + BsonDocument.parse("{'a': 1, 'z': ''}"), + a1.setField("z", a1.getString("missing").toLower())); + /* + The behaviour of missing fields appears to be as follows, and equivalent to $$REMOVE: + propagates -- getField, cond branches... + false -- not, or, cond check... + 0 -- sum... + "" -- toLower... + null -- multiply, add, subtract, year, filter, reduce, map, result within map... + */ + } + + @Test + public void setFieldTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/ + // Placing a field based on a literal: + assertExpression( + BsonDocument.parse("{a: 1, r: 2}"), // map.put("r", 2) + a1.setField("r", of(2)), + // MQL: + "{'$setField': {'field': 'r', 'input': {'$literal': {'a': 1}}, 'value': 2}}"); + + // Placing a null value: + assertExpression( + BsonDocument.parse("{a: 1, r: null}"), // map.put("r", null) + a1.setField("r", Expressions.ofNull()), + // MQL: + "{'$setField': {'field': 'r', 'input': {'$literal': {'a': 1}}, 'value': null}}"); + + // Replacing a field based on its prior value: + assertExpression( + BsonDocument.parse("{a: 3}"), // map.put("a", map.get("a") * 3) + a1.setField("a", a1.getInteger("a").multiply(3)), + // MQL: + "{'$setField': {'field': 'a', 'input': {'$literal': {'a': 1}}, 'value': " + + "{'$multiply': [{'$getField': {'input': {'$literal': {'a': 1}}, 'field': 'a'}}, 3]}}}"); + + // Placing a field based on a nested object: + assertExpression( + BsonDocument.parse("{'a': {'x': 1, 'y': 2}, r: 10}"), + ax1ay2.setField("r", ax1ay2.getDocument("a").getInteger("x").multiply(10)), + // MQL: + "{'$setField': {'field': 'r', 'input': {'$literal': {'a': {'x': 1, 'y': 2}}}, " + + "'value': {'$multiply': [" + + " {'$getField': {'input': {'$getField': {'input': {'$literal': {'a': {'x': 1, 'y': 2}}}, " + + " 'field': 'a'}}, 'field': 'x'}}, 10]}}}"); + + // Replacing a nested object requires two setFields, as expected: + assertExpression( + // "with" syntax: [ { a:{x:1,y:2} } ].map(d -> d.with("a", d.a.with("y", d.a.y.multiply(11)))) + BsonDocument.parse("{'a': {'x': 1, 'y': 22}}"), + ax1ay2.setField("a", ax1ay2.getDocument("a") + .setField("y", ax1ay2.getDocument("a").getInteger("y").multiply(11))), + "{'$setField': {'field': 'a', 'input': {'$literal': {'a': {'x': 1, 'y': 2}}}, " + + "'value': {'$setField': {'field': 'y', 'input': {'$getField': " + + "{'input': {'$literal': {'a': {'x': 1, 'y': 2}}}, 'field': 'a'}}, " + + "'value': {'$multiply': [{'$getField': {'input': {'$getField': " + + "{'input': {'$literal': {'a': {'x': 1, 'y': 2}}}, 'field': 'a'}}, " + + "'field': 'y'}}, 11]}}}}}"); + } + + @Test + public void unsetFieldTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/unsetField/ + assertExpression( + BsonDocument.parse("{}"), // map.remove("a") + a1.unsetField("a"), + // MQL: + "{'$unsetField': {'field': 'a', 'input': {'$literal': {'a': 1}}}}"); + } + + @Test + public void mergeTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/mergeObjects/ + assertExpression( + BsonDocument.parse("{a: 1, b: 2}"), + ofDoc("{a: 1}").merge(ofDoc("{b: 2}")), + "{'$mergeObjects': [{'$literal': {'a': 1}}, {'$literal': {'b': 2}}]}"); + + assertExpression( + BsonDocument.parse("{a: null}"), + ofDoc("{a: 1}").merge(ofDoc("{a: null}"))); + + assertExpression( + BsonDocument.parse("{a: 1}"), + ofDoc("{a: null}").merge(ofDoc("{a: 1}"))); + } +}