diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/ArrayExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/ArrayExpression.java index dc1e7b84261..7b499f7ada0 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/ArrayExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/ArrayExpression.java @@ -112,4 +112,8 @@ default ArrayExpression slice(final int start, final int length) { ArrayExpression union(ArrayExpression set); ArrayExpression distinct(); + + R passArrayTo(Function, ? extends R> f); + + R switchArrayOn(Function>, ? extends BranchesTerminal, ? extends R>> on); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/BooleanExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/BooleanExpression.java index fcf0eca939e..7f0b43370dc 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/BooleanExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/BooleanExpression.java @@ -16,6 +16,8 @@ package com.mongodb.client.model.expressions; +import java.util.function.Function; + /** * Expresses a boolean value. */ @@ -58,4 +60,8 @@ public interface BooleanExpression extends Expression { * @param The type of the resulting expression. */ T cond(T left, T right); + + R passBooleanTo(Function f); + + R switchBooleanOn(Function, ? extends BranchesTerminal> on); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/Branches.java b/driver-core/src/main/com/mongodb/client/model/expressions/Branches.java new file mode 100644 index 00000000000..40a726b4c9c --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/expressions/Branches.java @@ -0,0 +1,97 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public final class Branches { + + Branches() { + } + + private static BranchesIntermediary with(final Function> switchCase) { + List>> v = new ArrayList<>(); + v.add(switchCase); + return new BranchesIntermediary<>(v); + } + + private static MqlExpression mqlEx(final T value) { + return (MqlExpression) value; + } + + // is fn + + public BranchesIntermediary is(final Function o, final Function r) { + return with(value -> new SwitchCase<>(o.apply(value), r.apply(value))); + } + + // eq lt lte + + public BranchesIntermediary eq(final T v, final Function r) { + return is(value -> value.eq(v), r); + } + + public BranchesIntermediary lt(final T v, final Function r) { + return is(value -> value.lt(v), r); + } + + public BranchesIntermediary lte(final T v, final Function r) { + return is(value -> value.lte(v), r); + } + + // is type + + public BranchesIntermediary isBoolean(final Function r) { + return is(v -> mqlEx(v).isBoolean(), v -> r.apply((BooleanExpression) v)); + } + + public BranchesIntermediary isNumber(final Function r) { + return is(v -> mqlEx(v).isNumber(), v -> r.apply((NumberExpression) v)); + } + + public BranchesIntermediary isInteger(final Function r) { + return is(v -> mqlEx(v).isInteger(), v -> r.apply((IntegerExpression) v)); + } + + public BranchesIntermediary isString(final Function r) { + return is(v -> mqlEx(v).isString(), v -> r.apply((StringExpression) v)); + } + + public BranchesIntermediary isDate(final Function r) { + return is(v -> mqlEx(v).isDate(), v -> r.apply((DateExpression) v)); + } + + @SuppressWarnings("unchecked") + public BranchesIntermediary isArray(final Function, ? extends R> r) { + return is(v -> mqlEx(v).isArray(), v -> r.apply((ArrayExpression) v)); + } + + public BranchesIntermediary isDocument(final Function r) { + return is(v -> mqlEx(v).isDocumentOrMap(), v -> r.apply((DocumentExpression) v)); + } + + @SuppressWarnings("unchecked") + public BranchesIntermediary isMap(final Function, ? extends R> r) { + return is(v -> mqlEx(v).isDocumentOrMap(), v -> r.apply((MapExpression) v)); + } + + public BranchesIntermediary isNull(final Function r) { + return is(v -> mqlEx(v).isNull(), v -> r.apply(v)); + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/BranchesIntermediary.java b/driver-core/src/main/com/mongodb/client/model/expressions/BranchesIntermediary.java new file mode 100644 index 00000000000..9ef53d4a7c7 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/expressions/BranchesIntermediary.java @@ -0,0 +1,102 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public final class BranchesIntermediary extends BranchesTerminal { + BranchesIntermediary(final List>> branches) { + super(branches, null); + } + + private BranchesIntermediary with(final Function> switchCase) { + List>> v = new ArrayList<>(this.getBranches()); + v.add(switchCase); + return new BranchesIntermediary<>(v); + } + + private static MqlExpression mqlEx(final T value) { + return (MqlExpression) value; + } + + // is fn + + public BranchesIntermediary is(final Function o, final Function r) { + return this.with(value -> new SwitchCase<>(o.apply(value), r.apply(value))); + } + + // eq lt lte + + public BranchesIntermediary eq(final T v, final Function r) { + return is(value -> value.eq(v), r); + } + + public BranchesIntermediary lt(final T v, final Function r) { + return is(value -> value.lt(v), r); + } + + public BranchesIntermediary lte(final T v, final Function r) { + return is(value -> value.lte(v), r); + } + + // is type + + public BranchesIntermediary isBoolean(final Function r) { + return is(v -> mqlEx(v).isBoolean(), v -> r.apply((BooleanExpression) v)); + } + + public BranchesIntermediary isNumber(final Function r) { + return is(v -> mqlEx(v).isNumber(), v -> r.apply((NumberExpression) v)); + } + + public BranchesIntermediary isInteger(final Function r) { + return is(v -> mqlEx(v).isInteger(), v -> r.apply((IntegerExpression) v)); + } + + public BranchesIntermediary isString(final Function r) { + return is(v -> mqlEx(v).isString(), v -> r.apply((StringExpression) v)); + } + + public BranchesIntermediary isDate(final Function r) { + return is(v -> mqlEx(v).isDate(), v -> r.apply((DateExpression) v)); + } + + @SuppressWarnings("unchecked") + public BranchesIntermediary isArray(final Function, ? extends R> r) { + return is(v -> mqlEx(v).isArray(), v -> r.apply((ArrayExpression) v)); + } + + public BranchesIntermediary isDocument(final Function r) { + return is(v -> mqlEx(v).isDocumentOrMap(), v -> r.apply((DocumentExpression) v)); + } + + @SuppressWarnings("unchecked") + public BranchesIntermediary isMap(final Function, ? extends R> r) { + return is(v -> mqlEx(v).isDocumentOrMap(), v -> r.apply((MapExpression) v)); + } + + public BranchesIntermediary isNull(final Function r) { + return is(v -> mqlEx(v).isNull(), v -> r.apply(v)); + } + + public BranchesTerminal defaults(final Function r) { + return this.withDefault(value -> r.apply(value)); + } + +} diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/BranchesTerminal.java b/driver-core/src/main/com/mongodb/client/model/expressions/BranchesTerminal.java new file mode 100644 index 00000000000..97d0b85f233 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/expressions/BranchesTerminal.java @@ -0,0 +1,47 @@ +/* + * 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 com.mongodb.lang.Nullable; + +import java.util.List; +import java.util.function.Function; + +public class BranchesTerminal { + + private final List>> branches; + + private final Function defaults; + + BranchesTerminal(final List>> branches, @Nullable final Function defaults) { + this.branches = branches; + this.defaults = defaults; + } + + protected BranchesTerminal withDefault(final Function defaults) { + return new BranchesTerminal<>(branches, defaults); + } + + protected List>> getBranches() { + return branches; + } + + @Nullable + protected Function getDefaults() { + return defaults; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/DateExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/DateExpression.java index 04eb6b00ea5..cd211d1fd85 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/DateExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/DateExpression.java @@ -16,6 +16,8 @@ package com.mongodb.client.model.expressions; +import java.util.function.Function; + /** * Expresses a date value. */ @@ -33,4 +35,6 @@ public interface DateExpression extends Expression { StringExpression asString(StringExpression timezone, StringExpression format); + R passDateTo(Function f); + R switchDateOn(Function, ? extends BranchesTerminal> on); } 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 3ce4f8946b5..4d7d2ef678f 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 @@ -19,6 +19,7 @@ import org.bson.conversions.Bson; import java.time.Instant; +import java.util.function.Function; import static com.mongodb.client.model.expressions.Expressions.of; import static com.mongodb.client.model.expressions.Expressions.ofMap; @@ -100,4 +101,7 @@ default MapExpression getMap(final String fieldName, f DocumentExpression merge(DocumentExpression other); MapExpression asMap(); + + R passDocumentTo(Function f); + R switchDocumentOn(Function, ? extends BranchesTerminal> on); } 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 e2000d24aaa..a803134cc4f 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 @@ -18,6 +18,8 @@ import com.mongodb.annotations.Evolving; +import java.util.function.Function; + /** * Expressions express values that may be represented in (or computations that * may be performed within) a MongoDB server. Each expression evaluates to some @@ -114,4 +116,9 @@ public interface Expression { MapExpression isMapOr(MapExpression other); StringExpression asString(); + + R passTo(Function f); + + R switchOn(Function, ? extends BranchesTerminal> on); + } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/IntegerExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/IntegerExpression.java index eab541c0474..40cde216ce3 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/IntegerExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/IntegerExpression.java @@ -16,6 +16,8 @@ package com.mongodb.client.model.expressions; +import java.util.function.Function; + /** * Expresses an integer value. */ @@ -42,4 +44,7 @@ default IntegerExpression subtract(final int subtract) { IntegerExpression min(IntegerExpression i); IntegerExpression abs(); + + R passIntegerTo(Function f); + R switchIntegerOn(Function, ? extends BranchesTerminal> on); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/MapExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/MapExpression.java index 2271a77025d..d5d782c7728 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/MapExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/MapExpression.java @@ -16,6 +16,8 @@ package com.mongodb.client.model.expressions; +import java.util.function.Function; + import static com.mongodb.client.model.expressions.Expressions.of; public interface MapExpression extends Expression { @@ -57,4 +59,7 @@ default MapExpression unset(final String key) { ArrayExpression> entrySet(); R asDocument(); + + R passMapTo(Function, ? extends R> f); + R switchMapOn(Function>, ? extends BranchesTerminal, ? extends R>> on); } 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 e26643ddf80..2f62b05b11a 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 @@ -28,6 +28,7 @@ import java.util.function.Function; import static com.mongodb.client.model.expressions.Expressions.of; +import static com.mongodb.client.model.expressions.Expressions.ofNull; import static com.mongodb.client.model.expressions.Expressions.ofStringArray; final class MqlExpression @@ -282,6 +283,114 @@ public DocumentExpression unsetField(final String fieldName) { /** @see Expression */ + @Override + public R passTo(final Function f) { + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchOn(final Function, ? extends BranchesTerminal> switchMap) { + return switchMapInternal(this.assertImplementsAllExpressions(), switchMap.apply(new Branches<>())); + } + + @Override + public R passBooleanTo(final Function f) { + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchBooleanOn(final Function, ? extends BranchesTerminal> switchMap) { + return switchMapInternal(this.assertImplementsAllExpressions(), switchMap.apply(new Branches<>())); + } + + @Override + public R passIntegerTo(final Function f) { + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchIntegerOn(final Function, ? extends BranchesTerminal> switchMap) { + return switchMapInternal(this.assertImplementsAllExpressions(), switchMap.apply(new Branches<>())); + } + + @Override + public R passNumberTo(final Function f) { + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchNumberOn(final Function, ? extends BranchesTerminal> switchMap) { + return switchMapInternal(this.assertImplementsAllExpressions(), switchMap.apply(new Branches<>())); + } + + @Override + public R passStringTo(final Function f) { + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchStringOn(final Function, ? extends BranchesTerminal> switchMap) { + return switchMapInternal(this.assertImplementsAllExpressions(), switchMap.apply(new Branches<>())); + } + + @Override + public R passDateTo(final Function f) { + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchDateOn(final Function, ? extends BranchesTerminal> switchMap) { + return switchMapInternal(this.assertImplementsAllExpressions(), switchMap.apply(new Branches<>())); + } + + @Override + public R passArrayTo(final Function, ? extends R> f) { + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchArrayOn(final Function>, ? extends BranchesTerminal, ? extends R>> switchMap) { + return switchMapInternal(this.assertImplementsAllExpressions(), switchMap.apply(new Branches<>())); + } + + @Override + public R passMapTo(final Function, ? extends R> f) { + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchMapOn(final Function>, ? extends BranchesTerminal, ? extends R>> switchMap) { + return switchMapInternal(this.assertImplementsAllExpressions(), switchMap.apply(new Branches<>())); + } + + @Override + public R passDocumentTo(final Function f) { + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchDocumentOn(final Function, ? extends BranchesTerminal> switchMap) { + return switchMapInternal(this.assertImplementsAllExpressions(), switchMap.apply(new Branches<>())); + } + + private R0 switchMapInternal( + final T0 value, final BranchesTerminal construct) { + return newMqlExpression((cr) -> { + BsonArray branches = new BsonArray(); + for (Function> fn : construct.getBranches()) { + SwitchCase result = fn.apply(value); + branches.add(new BsonDocument() + .append("case", extractBsonValue(cr, result.getCaseValue())) + .append("then", extractBsonValue(cr, result.getThenValue()))); + } + BsonDocument switchBson = new BsonDocument().append("branches", branches); + if (construct.getDefaults() != null) { + switchBson = switchBson.append("default", extractBsonValue(cr, construct.getDefaults().apply(value))); + } + return astDoc("$switch", switchBson); + }); + } + @Override public BooleanExpression eq(final Expression eq) { return new MqlExpression<>(ast("$eq", eq)); @@ -313,7 +422,7 @@ public BooleanExpression lte(final Expression lte) { } public BooleanExpression isBoolean() { - return new MqlExpression<>(ast("$type")).eq(of("bool")); + return new MqlExpression<>(astWrapped("$type")).eq(of("bool")); } @Override @@ -331,16 +440,30 @@ public NumberExpression isNumberOr(final NumberExpression other) { } public BooleanExpression isInteger() { - return this.isNumber().cond(this.eq(this.round()), of(false)); + return switchOn(on -> on + .isNumber(v -> v.round().eq(v)) + .defaults(v -> of(false))); } @Override public IntegerExpression isIntegerOr(final IntegerExpression other) { - return this.isInteger().cond(this, other); + /* + The server does not evaluate both branches of and/or/cond unless needed. + However, the server has a pipeline optimization stage prior to + evaluation that does attempt to optimize both branches, and fails with + "Failed to optimize pipeline" when there is a problem arising from the + use of literals and typed expressions. Using "switch" avoids this, + otherwise we could just use: + this.isNumber().and(this.eq(this.round())) + */ + + return this.switchOn(on -> on + .isNumber(v -> (IntegerExpression) v.round().eq(v).cond(v, other)) + .defaults(v -> other)); } public BooleanExpression isString() { - return new MqlExpression<>(ast("$type")).eq(of("string")); + return new MqlExpression<>(astWrapped("$type")).eq(of("string")); } @Override @@ -349,7 +472,7 @@ public StringExpression isStringOr(final StringExpression other) { } public BooleanExpression isDate() { - return ofStringArray("date").contains(new MqlExpression<>(ast("$type"))); + return ofStringArray("date").contains(new MqlExpression<>(astWrapped("$type"))); } @Override @@ -362,7 +485,7 @@ public BooleanExpression isArray() { } private Expression ifNull(final Expression ifNull) { - return new MqlExpression<>(ast("$ifNull", ifNull, Expressions.ofNull())) + return new MqlExpression<>(ast("$ifNull", ifNull, ofNull())) .assertImplementsAllExpressions(); } @@ -380,7 +503,7 @@ public ArrayExpression isArrayOr(final ArrayExpression return (ArrayExpression) this.isArray().cond(this.assertImplementsAllExpressions(), other); } - private BooleanExpression isDocumentOrMap() { + BooleanExpression isDocumentOrMap() { return new MqlExpression<>(ast("$type")).eq(of("object")); } @@ -395,6 +518,10 @@ public MapExpression isMapOr(final MapExpression(astWrapped("$toString")); diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/NumberExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/NumberExpression.java index f08b9a57d00..91d922dd476 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/NumberExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/NumberExpression.java @@ -16,6 +16,8 @@ package com.mongodb.client.model.expressions; +import java.util.function.Function; + /** * Expresses a numeric value. */ @@ -56,4 +58,7 @@ default NumberExpression subtract(final Number subtract) { NumberExpression abs(); DateExpression millisecondsToDate(); + + R passNumberTo(Function f); + R switchNumberOn(Function, ? extends BranchesTerminal> on); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/StringExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/StringExpression.java index c5cc7a8eb72..f4990b54949 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/StringExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/StringExpression.java @@ -16,6 +16,8 @@ package com.mongodb.client.model.expressions; +import java.util.function.Function; + import static com.mongodb.client.model.expressions.Expressions.of; /** @@ -52,4 +54,7 @@ default StringExpression substrBytes(final int start, final int length) { DateExpression parseDate(StringExpression format); DateExpression parseDate(StringExpression timezone, StringExpression format); + + R passStringTo(Function f); + R switchStringOn(Function, ? extends BranchesTerminal> on); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/SwitchCase.java b/driver-core/src/main/com/mongodb/client/model/expressions/SwitchCase.java new file mode 100644 index 00000000000..bbdc608391c --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/expressions/SwitchCase.java @@ -0,0 +1,35 @@ +/* + * 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; + +final class SwitchCase { + private final BooleanExpression caseValue; + private final R thenValue; + + SwitchCase(final BooleanExpression caseValue, final R thenValue) { + this.caseValue = caseValue; + this.thenValue = thenValue; + } + + BooleanExpression getCaseValue() { + return caseValue; + } + + R getThenValue() { + return thenValue; + } +} 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 b622688b49f..a72fbcd29fc 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 @@ -321,8 +321,9 @@ public void elementAtTest() { // 0.0 is a valid integer value array123.elementAt(of(0.0).isIntegerOr(of(-1))), // MQL: - "{'$arrayElemAt': [[1, 2, 3], {'$cond': [{'$cond': " - + "[{'$isNumber': [0.0]}, {'$eq': [0.0, {'$round': 0.0}]}, false]}, 0.0, -1]}]}"); + "{'$arrayElemAt': [[1, 2, 3], {'$switch': {'branches': [{'case': " + + "{'$isNumber': [0.0]}, 'then': {'$cond': " + + "[{'$eq': [{'$round': 0.0}, 0.0]}, 0.0, -1]}}], 'default': -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 b044128f039..1dc30aea020 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 @@ -44,6 +44,7 @@ class ComparisonExpressionsFunctionalTest extends AbstractExpressionsFunctionalT ofNull(), of(0), of(1), + of(2.0), of(""), of("str"), of(BsonDocument.parse("{}")), @@ -60,6 +61,17 @@ class ComparisonExpressionsFunctionalTest extends AbstractExpressionsFunctionalT of(Instant.now()) ); + @Test + public void literalsTest() { + // special values + assertExpression(null, ofNull(), "null"); + // the "missing" value is obtained via getField. + // the "$$REMOVE" value is intentionally not exposed. It is used internally. + // the "undefined" value is deprecated. + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/literal/ + // $literal is intentionally not exposed. It is used internally. + } + @Test public void eqTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/eq/ diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ControlExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ControlExpressionsFunctionalTest.java new file mode 100644 index 00000000000..77a539f418d --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ControlExpressionsFunctionalTest.java @@ -0,0 +1,279 @@ +/* + * 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.Document; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.function.Function; + +import static com.mongodb.client.model.expressions.Expressions.of; +import static com.mongodb.client.model.expressions.Expressions.ofArray; +import static com.mongodb.client.model.expressions.Expressions.ofIntegerArray; +import static com.mongodb.client.model.expressions.Expressions.ofMap; +import static com.mongodb.client.model.expressions.Expressions.ofNull; + +class ControlExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest { + + @Test + public void passToTest() { + Function intDecrement = (e) -> e.subtract(of(1)); + Function numDecrement = (e) -> e.subtract(of(1)); + + // "nested functional" function application: + assertExpression( + 2 - 1, + intDecrement.apply(of(2)), + "{'$subtract': [2, 1]}"); + // "chained" function application produces the same MQL: + assertExpression( + 2 - 1, + of(2).passIntegerTo(intDecrement), + "{'$subtract': [2, 1]}"); + + // variations + assertExpression( + 2 - 1, + of(2).passIntegerTo(numDecrement)); + assertExpression( + 2 - 1, + of(2).passNumberTo(numDecrement)); + + // all types + Function test = on -> of("A"); + assertExpression("A", of(true).passTo(test)); + assertExpression("A", of(false).passBooleanTo(test)); + assertExpression("A", of(0).passIntegerTo(test)); + assertExpression("A", of(0).passNumberTo(test)); + assertExpression("A", of("").passStringTo(test)); + assertExpression("A", of(Instant.ofEpochMilli(123)).passDateTo(test)); + assertExpression("A", ofIntegerArray(1, 2).passArrayTo(test)); + assertExpression("A", of(Document.parse("{_id: 'a'}")).passDocumentTo(test)); + assertExpression("A", ofMap(Document.parse("{_id: 'a'}")).passMapTo(test)); + } + + @Test + public void switchTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/switch/ + assertExpression("a", of(0).switchOn(on -> on.is(v -> v.eq(of(0)), v -> of("a")))); + assertExpression("a", of(0).switchOn(on -> on.isNumber(v -> of("a")))); + assertExpression("a", of(0).switchOn(on -> on.eq(of(0), v -> of("a")))); + assertExpression("a", of(0).switchOn(on -> on.lte(of(9), v -> of("a")))); + + // test branches + Function isOver10 = v -> v.subtract(10).gt(of(0)); + Function s = e -> e + .switchIntegerOn(on -> on + .eq(of(0), v -> of("A")) + .lt(of(10), v -> of("B")) + .is(isOver10, v -> of("C")) + .defaults(v -> of("D"))) + .toLower(); + + assertExpression("a", of(0).passIntegerTo(s)); + assertExpression("b", of(9).passIntegerTo(s)); + assertExpression("b", of(-9).passIntegerTo(s)); + assertExpression("c", of(11).passIntegerTo(s)); + assertExpression("d", of(10).passIntegerTo(s)); + } + + @Test + public void switchInferenceTest() { + // the following must compile: + assertExpression( + "b", + of(1).switchOn(on -> on + .eq(of(0), v -> of("a")) + .eq(of(1), v -> of("b")) + )); + // the "of(0)" must not cause a type inference of T being an integer, + // since switchOn expects an Expression. + } + + @Test + public void switchTypesTest() { + Function label = expr -> expr.switchOn(on -> on + .isBoolean(v -> v.asString().concat(of(" - bool"))) + // integer should be checked before string + .isInteger(v -> v.asString().concat(of(" - integer"))) + .isNumber(v -> v.asString().concat(of(" - number"))) + .isString(v -> v.asString().concat(of(" - string"))) + .isDate(v -> v.asString().concat(of(" - date"))) + .isArray((ArrayExpression v) -> v.sum(a -> a).asString().concat(of(" - array"))) + .isDocument(v -> v.getString("_id").concat(of(" - document"))) + .isNull(v -> of("null - null")) + .defaults(v -> of("default")) + ).toLower(); + assertExpression("true - bool", of(true).passTo(label)); + assertExpression("false - bool", of(false).passBooleanTo(label)); + assertExpression("1 - integer", of(1).passIntegerTo(label)); + assertExpression("1 - integer", of(1.0).passNumberTo(label)); + assertExpression("1.01 - number", of(1.01).passNumberTo(label)); + assertExpression("abc - string", of("abc").passStringTo(label)); + assertExpression("1970-01-01t00:00:00.123z - date", of(Instant.ofEpochMilli(123)).passDateTo(label)); + assertExpression("3 - array", ofIntegerArray(1, 2).passArrayTo(label)); + assertExpression("a - document", of(Document.parse("{_id: 'a'}")).passDocumentTo(label)); + // maps are considered documents + assertExpression("a - document", ofMap(Document.parse("{_id: 'a'}")).passMapTo(label)); + assertExpression("null - null", ofNull().passTo(label)); + // maps via isMap: + assertExpression( + "12 - map", + ofMap(Document.parse("{a: '1', b: '2'}")).switchOn(on -> on + .isMap((MapExpression v) -> v.entrySet() + .join(e -> e.getValue()).concat(of(" - map"))))); + // arrays via isArray, and tests signature: + assertExpression( + "ab - array", + ofArray(of("a"), of("b")).switchOn(on -> on + .isArray((ArrayExpression v) -> v + .join(e -> e).concat(of(" - array"))))); + } + + private BranchesIntermediary branches(final Branches on) { + return on.is(v -> of(true), v -> of("A")); + } + + @Test + public void switchTestVariants() { + assertExpression("A", of(true).switchOn(this::branches)); + assertExpression("A", of(false).switchBooleanOn(this::branches)); + assertExpression("A", of(0).switchIntegerOn(this::branches)); + assertExpression("A", of(0).switchNumberOn(this::branches)); + assertExpression("A", of("").switchStringOn(this::branches)); + assertExpression("A", of(Instant.ofEpochMilli(123)).switchDateOn(this::branches)); + assertExpression("A", ofIntegerArray(1, 2).switchArrayOn(this::branches)); + assertExpression("A", of(Document.parse("{_id: 'a'}")).switchDocumentOn(this::branches)); + assertExpression("A", ofMap(Document.parse("{_id: 'a'}")).switchMapOn(this::branches)); + } + + @Test + public void switchTestInitial() { + assertExpression("A", + of(0).switchOn(on -> on.is(v -> v.gt(of(-1)), v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$gt': [0, -1]}, 'then': 'A'}]}}"); + // eq lt lte + assertExpression("A", + of(0).switchOn(on -> on.eq(of(0), v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [0, 0]}, 'then': 'A'}]}}"); + assertExpression("A", + of(0).switchOn(on -> on.lt(of(1), v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$lt': [0, 1]}, 'then': 'A'}]}}"); + assertExpression("A", + of(0).switchOn(on -> on.lte(of(0), v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$lte': [0, 0]}, 'then': 'A'}]}}"); + // is type + assertExpression("A", + of(true).switchOn(on -> on.isBoolean(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [{'$type': [true]}, 'bool']}, 'then': 'A'}]}}"); + assertExpression("A", + of(1).switchOn(on -> on.isNumber(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$isNumber': [1]}, 'then': 'A'}]}}"); + assertExpression("A", + of(1).switchOn(on -> on.isInteger(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$switch': {'branches': [{'case': {'$isNumber': [1]}," + + "'then': {'$eq': [{'$round': 1}, 1]}}], 'default': false}}, 'then': 'A'}]}}"); + assertExpression("A", + of("x").switchOn(on -> on.isString(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [{'$type': ['x']}, 'string']}, 'then': 'A'}]}}"); + assertExpression("A", + of(Instant.ofEpochMilli(123)).switchOn(on -> on.isDate(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$in': [{'$type': " + + "[{'$date': '1970-01-01T00:00:00.123Z'}]}, ['date']]}, 'then': 'A'}]}}"); + assertExpression("A", + ofIntegerArray(0).switchOn(on -> on.isArray(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$isArray': [[0]]}, 'then': 'A'}]}}"); + assertExpression("A", + of(Document.parse("{}")).switchOn(on -> on.isDocument(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [{'$type': " + + "[{'$literal': {}}]}, 'object']}, 'then': 'A'}]}}"); + assertExpression("A", + ofMap(Document.parse("{}")).switchOn(on -> on.isMap(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [{'$type': " + + "{'$literal': {}}}, 'object']}, 'then': 'A'}]}}"); + assertExpression("A", + ofNull().switchOn(on -> on.isNull(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [null, null]}, 'then': 'A'}]}}"); + } + + @Test + public void switchTestPartial() { + assertExpression("A", + of(0).switchOn(on -> on.isNull(v -> of("X")).is(v -> v.gt(of(-1)), v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [0, null]}, 'then': 'X'}, " + + "{'case': {'$gt': [0, -1]}, 'then': 'A'}]}}"); + assertExpression("A", + of(0).switchOn(on -> on.isNull(v -> of("X")).defaults(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [0, null]}, 'then': 'X'}], " + + "'default': 'A'}}"); + // eq lt lte + assertExpression("A", + of(0).switchOn(on -> on.isNull(v -> of("X")).eq(of(0), v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [0, null]}, 'then': 'X'}, " + + "{'case': {'$eq': [0, 0]}, 'then': 'A'}]}}"); + assertExpression("A", + of(0).switchOn(on -> on.isNull(v -> of("X")).lt(of(1), v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [0, null]}, 'then': 'X'}, " + + "{'case': {'$lt': [0, 1]}, 'then': 'A'}]}}"); + assertExpression("A", + of(0).switchOn(on -> on.isNull(v -> of("X")).lte(of(0), v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [0, null]}, 'then': 'X'}, " + + "{'case': {'$lte': [0, 0]}, 'then': 'A'}]}}"); + // is type + assertExpression("A", + of(true).switchOn(on -> on.isNull(v -> of("X")).isBoolean(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [true, null]}, 'then': 'X'}, " + + "{'case': {'$eq': [{'$type': [true]}, 'bool']}, 'then': 'A'}]}}"); + assertExpression("A", + of(1).switchOn(on -> on.isNull(v -> of("X")).isNumber(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [1, null]}, 'then': 'X'}, " + + "{'case': {'$isNumber': [1]}, 'then': 'A'}]}}"); + assertExpression("A", + of(1).switchOn(on -> on.isNull(v -> of("X")).isInteger(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [1, null]}, 'then': 'X'}, {'case': " + + "{'$switch': {'branches': [{'case': {'$isNumber': [1]}, " + + "'then': {'$eq': [{'$round': 1}, 1]}}], 'default': false}}, 'then': 'A'}]}}"); + assertExpression("A", + of("x").switchOn(on -> on.isNull(v -> of("X")).isString(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': ['x', null]}, 'then': 'X'}, " + + "{'case': {'$eq': [{'$type': ['x']}, 'string']}, 'then': 'A'}]}}"); + assertExpression("A", + of(Instant.ofEpochMilli(123)).switchOn(on -> on.isNull(v -> of("X")).isDate(v -> of("A"))), + "{'$switch': {'branches': [" + + "{'case': {'$eq': [{'$date': '1970-01-01T00:00:00.123Z'}, null]}, 'then': 'X'}, " + + "{'case': {'$in': [{'$type': [{'$date': '1970-01-01T00:00:00.123Z'}]}, " + + "['date']]}, 'then': 'A'}]}}"); + assertExpression("A", + ofIntegerArray(0).switchOn(on -> on.isNull(v -> of("X")).isArray(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [[0], null]}, 'then': 'X'}, " + + "{'case': {'$isArray': [[0]]}, 'then': 'A'}]}}"); + assertExpression("A", + of(Document.parse("{}")).switchOn(on -> on.isNull(v -> of("X")).isDocument(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$eq': [{'$literal': {}}, null]}, 'then': 'X'}, " + + "{'case': {'$eq': [{'$type': [{'$literal': {}}]}, 'object']}, 'then': 'A'}]}}"); + assertExpression("A", + ofMap(Document.parse("{}")).switchOn(on -> on.isNull(v -> of("X")).isMap(v -> of("A"))), + " {'$switch': {'branches': [" + + "{'case': {'$eq': [{'$literal': {}}, null]}, 'then': 'X'}, " + + "{'case': {'$eq': [{'$type': {'$literal': {}}}, 'object']}, 'then': 'A'}]}}"); + assertExpression("A", + ofNull().switchOn(on -> on.isNumber(v -> of("X")).isNull(v -> of("A"))), + "{'$switch': {'branches': [{'case': {'$isNumber': [null]}, 'then': 'X'}, " + + "{'case': {'$eq': [null, null]}, 'then': 'A'}]}}"); + } +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java index adb4a82e9ce..db600647ab5 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java @@ -46,7 +46,7 @@ public void isBooleanOrTest() { assertExpression( true, of(true).isBooleanOr(of(false)), - "{'$cond': [{'$eq': [{'$type': true}, 'bool']}, true, false]}"); + "{'$cond': [{'$eq': [{'$type': [true]}, 'bool']}, true, false]}"); // non-boolean: assertExpression(false, ofIntegerArray(1).isBooleanOr(of(false))); assertExpression(false, ofNull().isBooleanOr(of(false))); @@ -65,12 +65,30 @@ public void isNumberOrTest() { assertExpression(99, ofNull().isNumberOr(of(99))); } + @Test + public void isIntegerOr() { + assertExpression( + 1, + of(1).isIntegerOr(of(99)), + "{'$switch': {'branches': [{'case': {'$isNumber': [1]}, 'then': " + + "{'$cond': [{'$eq': [{'$round': 1}, 1]}, 1, 99]}}], 'default': 99}}" + ); + // other numeric values: + assertExpression(1L, of(1L).isIntegerOr(of(99))); + assertExpression(1.0, of(1.0).isIntegerOr(of(99))); + assertExpression(Decimal128.parse("1"), of(Decimal128.parse("1")).isIntegerOr(of(99))); + // non-numeric: + assertExpression(99, ofIntegerArray(1).isIntegerOr(of(99))); + assertExpression(99, ofNull().isIntegerOr(of(99))); + assertExpression(99, of("str").isIntegerOr(of(99))); + } + @Test public void isStringOrTest() { assertExpression( "abc", of("abc").isStringOr(of("or")), - "{'$cond': [{'$eq': [{'$type': 'abc'}, 'string']}, 'abc', 'or']}"); + "{'$cond': [{'$eq': [{'$type': ['abc']}, 'string']}, 'abc', 'or']}"); // non-string: assertExpression("or", ofIntegerArray(1).isStringOr(of("or"))); assertExpression("or", ofNull().isStringOr(of("or"))); @@ -82,7 +100,7 @@ public void isDateOrTest() { assertExpression( date, of(date).isDateOr(of(date.plusMillis(10))), - "{'$cond': [{'$in': [{'$type': {'$date': '2007-12-03T10:15:30.005Z'}}, ['date']]}, " + "{'$cond': [{'$in': [{'$type': [{'$date': '2007-12-03T10:15:30.005Z'}]}, ['date']]}, " + "{'$date': '2007-12-03T10:15:30.005Z'}, {'$date': '2007-12-03T10:15:30.015Z'}]}"); // non-date: assertExpression(date, ofIntegerArray(1).isDateOr(of(date))); @@ -107,7 +125,7 @@ public void isDocumentOrTest() { assertExpression( doc, of(doc).isDocumentOr(of(BsonDocument.parse("{b: 2}"))), - "{'$cond': [{'$eq': [{'$type': {'$literal': {'a': 1}}}, 'object']}, " + "{'$cond': [{'$eq': [{'$type': [{'$literal': {'a': 1}}]}, 'object']}, " + "{'$literal': {'a': 1}}, {'$literal': {'b': 2}}]}"); // non-document: assertExpression(doc, ofIntegerArray(1).isDocumentOr(of(doc)));