diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index a5ff7ca9304..49f3fb18e9e 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -157,4 +157,6 @@ + + diff --git a/driver-core/src/main/com/mongodb/MongoClientSettings.java b/driver-core/src/main/com/mongodb/MongoClientSettings.java index f81b6a75b1f..524f0100e4f 100644 --- a/driver-core/src/main/com/mongodb/MongoClientSettings.java +++ b/driver-core/src/main/com/mongodb/MongoClientSettings.java @@ -19,6 +19,7 @@ import com.mongodb.annotations.Immutable; import com.mongodb.annotations.NotThreadSafe; import com.mongodb.client.gridfs.codecs.GridFSFileCodecProvider; +import com.mongodb.client.model.mql.ExpressionCodecProvider; import com.mongodb.client.model.geojson.codecs.GeoJsonCodecProvider; import com.mongodb.connection.ClusterSettings; import com.mongodb.connection.ConnectionPoolSettings; @@ -76,6 +77,7 @@ public final class MongoClientSettings { new JsonObjectCodecProvider(), new BsonCodecProvider(), new EnumCodecProvider(), + new ExpressionCodecProvider(), new Jep395RecordCodecProvider())); private final ReadPreference readPreference; @@ -123,6 +125,7 @@ public final class MongoClientSettings { *
  • {@link org.bson.codecs.JsonObjectCodecProvider}
  • *
  • {@link org.bson.codecs.BsonCodecProvider}
  • *
  • {@link org.bson.codecs.EnumCodecProvider}
  • + *
  • {@link ExpressionCodecProvider}
  • *
  • {@link com.mongodb.Jep395RecordCodecProvider}
  • * * diff --git a/driver-core/src/main/com/mongodb/annotations/Sealed.java b/driver-core/src/main/com/mongodb/annotations/Sealed.java new file mode 100644 index 00000000000..59a2b2a7473 --- /dev/null +++ b/driver-core/src/main/com/mongodb/annotations/Sealed.java @@ -0,0 +1,42 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright 2010 The Guava Authors + * Copyright 2011 The Guava Authors + * + * 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.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Signifies that the annotated class or interface should be treated as sealed: + * it must not be extended or implemented. + * + *

    Using such classes and interfaces is no different from using ordinary + * unannotated classes and interfaces. + * + *

    This annotation does not imply that the API is experimental or + * {@link Beta}, or that the quality or performance of the API is inferior. + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +@Documented +@Sealed +public @interface Sealed { +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/Branches.java b/driver-core/src/main/com/mongodb/client/model/mql/Branches.java new file mode 100644 index 00000000000..1a576cfe581 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/Branches.java @@ -0,0 +1,268 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.assertions.Assertions; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import static com.mongodb.client.model.mql.MqlUnchecked.Unchecked.TYPE_ARGUMENT; + +/** + * Branches are used in {@linkplain MqlValue#switchOn}, and + * define a sequence of checks that will be performed. The first check + * to succeed will produce the value that it specifies. If no check succeeds, + * then the operation + * {@linkplain BranchesIntermediary#defaults(Function) defaults} to a default + * value, or if none is specified, the operation will cause an error. + * + * @param the type of the values that may be checked. + * @since 4.9.0 + */ +@Beta(Beta.Reason.CLIENT) +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 + + /** + * A successful check for the specified {@code predicate} + * produces a value specified by the {@code mapping}. + * + * @param predicate the predicate. + * @param mapping the mapping. + * @param the type of the produced value. + * @return the appended sequence of checks. + */ + public BranchesIntermediary is(final Function predicate, final Function mapping) { + Assertions.notNull("predicate", predicate); + Assertions.notNull("mapping", mapping); + return with(value -> new SwitchCase<>(predicate.apply(value), mapping.apply(value))); + } + + // eq lt lte + + /** + * A successful check for {@linkplain MqlValue#eq equality} + * produces a value specified by the {@code mapping}. + * + * @param v the value to check against. + * @param mapping the mapping. + * @param the type of the produced value. + * @return the appended sequence of checks. + */ + public BranchesIntermediary eq(final T v, final Function mapping) { + Assertions.notNull("v", v); + Assertions.notNull("mapping", mapping); + return is(value -> value.eq(v), mapping); + } + + /** + * A successful check for being + * {@linkplain MqlValue#lt less than} + * the provided value {@code v} + * produces a value specified by the {@code mapping}. + * + * @param v the value to check against. + * @param mapping the mapping. + * @param the type of the produced value. + * @return the appended sequence of checks. + */ + public BranchesIntermediary lt(final T v, final Function mapping) { + Assertions.notNull("v", v); + Assertions.notNull("mapping", mapping); + return is(value -> value.lt(v), mapping); + } + + /** + * A successful check for being + * {@linkplain MqlValue#lte less than or equal to} + * the provided value {@code v} + * produces a value specified by the {@code mapping}. + * + * @param v the value to check against. + * @param mapping the mapping. + * @param the type of the produced value. + * @return the appended sequence of checks. + */ + public BranchesIntermediary lte(final T v, final Function mapping) { + Assertions.notNull("v", v); + Assertions.notNull("mapping", mapping); + return is(value -> value.lte(v), mapping); + } + + // is type + + /** + * A successful check for + * {@linkplain MqlValue#isBooleanOr(MqlBoolean) being a boolean} + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the produced value. + */ + public BranchesIntermediary isBoolean(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isBoolean(), v -> mapping.apply((MqlBoolean) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isNumberOr(MqlNumber) being a number} + * produces a value specified by the {@code mapping}. + * + * @mongodb.server.release 4.4 + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the produced value. + */ + public BranchesIntermediary isNumber(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isNumber(), v -> mapping.apply((MqlNumber) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isIntegerOr(MqlInteger) being an integer} + * produces a value specified by the {@code mapping}. + * + * @mongodb.server.release 4.4 + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the produced value. + */ + public BranchesIntermediary isInteger(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isInteger(), v -> mapping.apply((MqlInteger) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isStringOr(MqlString) being a string} + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the produced value. + */ + public BranchesIntermediary isString(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isString(), v -> mapping.apply((MqlString) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isDateOr(MqlDate) being a date} + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the produced value. + */ + public BranchesIntermediary isDate(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isDate(), v -> mapping.apply((MqlDate) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isArrayOr(MqlArray) being an array} + * produces a value specified by the {@code mapping}. + * + *

    Warning: The type argument of the array is not + * enforced by the API. The use of this method is an + * unchecked assertion that the type argument is correct. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the produced value. + * @param the type of the array. + */ + @SuppressWarnings("unchecked") + public BranchesIntermediary isArray(final Function, ? extends R> mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isArray(), v -> mapping.apply((MqlArray) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isDocumentOr(MqlDocument) being a document} + * (or document-like value, see + * {@link MqlMap} and {@link MqlEntry}) + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the produced value. + */ + public BranchesIntermediary isDocument(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isDocumentOrMap(), v -> mapping.apply((MqlDocument) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isMapOr(MqlMap) being a map} + * (or map-like value, see + * {@link MqlDocument} and {@link MqlEntry}) + * produces a value specified by the {@code mapping}. + * + *

    Warning: The type argument of the map is not + * enforced by the API. The use of this method is an + * unchecked assertion that the type argument is correct. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the produced value. + * @param the type of the array. + */ + @SuppressWarnings("unchecked") + public BranchesIntermediary isMap(final Function, ? extends R> mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isDocumentOrMap(), v -> mapping.apply((MqlMap) v)); + } + + /** + * A successful check for + * {@linkplain MqlValues#ofNull()} being the null value} + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the produced value. + */ + public BranchesIntermediary isNull(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isNull(), v -> mapping.apply(v)); + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/BranchesIntermediary.java b/driver-core/src/main/com/mongodb/client/model/mql/BranchesIntermediary.java new file mode 100644 index 00000000000..9b1b88e4467 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/BranchesIntermediary.java @@ -0,0 +1,264 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.assertions.Assertions; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import static com.mongodb.client.model.mql.MqlUnchecked.Unchecked.TYPE_ARGUMENT; + +/** + * See {@link Branches}. + * + * @param the type of the values that may be checked. + * @param the type of the value produced. + * @since 4.9.0 + */ +@Beta(Beta.Reason.CLIENT) +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 + + /** + * A successful check for the specified {@code predicate} + * produces a value specified by the {@code mapping}. + * + * @param predicate the predicate. + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary is(final Function predicate, final Function mapping) { + Assertions.notNull("predicate", predicate); + Assertions.notNull("mapping", mapping); + return this.with(value -> new SwitchCase<>(predicate.apply(value), mapping.apply(value))); + } + + // eq lt lte + + /** + * A successful check for {@linkplain MqlValue#eq equality} + * produces a value specified by the {@code mapping}. + * + * @param v the value to check against. + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary eq(final T v, final Function mapping) { + Assertions.notNull("v", v); + Assertions.notNull("mapping", mapping); + return is(value -> value.eq(v), mapping); + } + + /** + * A successful check for being + * {@linkplain MqlValue#lt less than} + * the provided value {@code v} + * produces a value specified by the {@code mapping}. + * + * @param v the value to check against. + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary lt(final T v, final Function mapping) { + Assertions.notNull("v", v); + Assertions.notNull("mapping", mapping); + return is(value -> value.lt(v), mapping); + } + + /** + * A successful check for being + * {@linkplain MqlValue#lte less than or equal to} + * the provided value {@code v} + * produces a value specified by the {@code mapping}. + * + * @param v the value to check against. + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary lte(final T v, final Function mapping) { + Assertions.notNull("v", v); + Assertions.notNull("mapping", mapping); + return is(value -> value.lte(v), mapping); + } + + // is type + + /** + * A successful check for + * {@linkplain MqlValue#isBooleanOr(MqlBoolean) being a boolean} + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary isBoolean(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isBoolean(), v -> mapping.apply((MqlBoolean) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isNumberOr(MqlNumber) being a number} + * produces a value specified by the {@code mapping}. + * + * @mongodb.server.release 4.4 + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary isNumber(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isNumber(), v -> mapping.apply((MqlNumber) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isIntegerOr(MqlInteger) being an integer} + * produces a value specified by the {@code mapping}. + * + * @mongodb.server.release 4.4 + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary isInteger(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isInteger(), v -> mapping.apply((MqlInteger) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isStringOr(MqlString) being a string} + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary isString(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isString(), v -> mapping.apply((MqlString) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isDateOr(MqlDate) being a date} + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary isDate(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isDate(), v -> mapping.apply((MqlDate) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isArrayOr(MqlArray) being an array} + * produces a value specified by the {@code mapping}. + * + *

    Warning: The type argument of the array is not + * enforced by the API. The use of this method is an + * unchecked assertion that the type argument is correct. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the elements of the resulting array. + */ + @SuppressWarnings("unchecked") + public BranchesIntermediary isArray(final Function, ? extends R> mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isArray(), v -> mapping.apply((MqlArray) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isDocumentOr(MqlDocument) being a document} + * (or document-like value, see + * {@link MqlMap} and {@link MqlEntry}) + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary isDocument(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isDocumentOrMap(), v -> mapping.apply((MqlDocument) v)); + } + + /** + * A successful check for + * {@linkplain MqlValue#isMapOr(MqlMap) being a map} + * (or map-like value, see + * {@link MqlDocument} and {@link MqlEntry}) + * produces a value specified by the {@code mapping}. + * + *

    Warning: The type argument of the map is not + * enforced by the API. The use of this method is an + * unchecked assertion that the type argument is correct. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + * @param the type of the array. + */ + @SuppressWarnings("unchecked") + public BranchesIntermediary isMap(final Function, ? extends R> mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isDocumentOrMap(), v -> mapping.apply((MqlMap) v)); + } + + /** + * A successful check for + * {@linkplain MqlValues#ofNull()} being the null value} + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesIntermediary isNull(final Function mapping) { + Assertions.notNull("mapping", mapping); + return is(v -> mqlEx(v).isNull(), v -> mapping.apply(v)); + } + + /** + * If no other check succeeds, + * produces a value specified by the {@code mapping}. + * + * @param mapping the mapping. + * @return the appended sequence of checks. + */ + public BranchesTerminal defaults(final Function mapping) { + Assertions.notNull("mapping", mapping); + return this.withDefault(value -> mapping.apply(value)); + } + +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/BranchesTerminal.java b/driver-core/src/main/com/mongodb/client/model/mql/BranchesTerminal.java new file mode 100644 index 00000000000..f72cb5cb1f4 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/BranchesTerminal.java @@ -0,0 +1,57 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.lang.Nullable; + +import java.util.List; +import java.util.function.Function; + +/** + * See {@link Branches}. This is the terminal branch, to which no additional + * checks may be added, since the default value has been specified. + * + * @param the type of the values that may be checked. + * @param the type of the value produced. + * @since 4.9.0 + */ +@Beta(Beta.Reason.CLIENT) +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; + } + + BranchesTerminal withDefault(final Function defaults) { + return new BranchesTerminal<>(branches, defaults); + } + + List>> getBranches() { + return branches; + } + + @Nullable + Function getDefaults() { + return defaults; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/ExpressionCodecProvider.java b/driver-core/src/main/com/mongodb/client/model/mql/ExpressionCodecProvider.java new file mode 100644 index 00000000000..d4176b7205f --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/ExpressionCodecProvider.java @@ -0,0 +1,50 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Immutable; +import com.mongodb.lang.Nullable; +import org.bson.codecs.Codec; +import org.bson.codecs.configuration.CodecProvider; +import org.bson.codecs.configuration.CodecRegistry; + +/** + * Provides Codec instances for the {@link MqlValue MQL API}. + * + *

    Responsible for converting values and computations expressed using the + * driver's implementation of the {@link MqlValue MQL API} into the corresponding + * values and computations expressed in MQL BSON. Booleans are converted to BSON + * booleans, documents to BSON documents, and so on. The specific structure + * representing numbers is preserved where possible (that is, number literals + * specified as Java longs are converted into BSON int64, and so on). + * + * @since 4.9.0 + */ +@Beta(Beta.Reason.CLIENT) +@Immutable +public final class ExpressionCodecProvider implements CodecProvider { + @Override + @SuppressWarnings("unchecked") + @Nullable + public Codec get(final Class clazz, final CodecRegistry registry) { + if (MqlExpression.class.equals(clazz)) { + return (Codec) new MqlExpressionCodec(registry); + } + return null; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlArray.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlArray.java new file mode 100644 index 00000000000..047e294c8e9 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlArray.java @@ -0,0 +1,359 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; + +import java.util.function.Function; + +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlUnchecked.Unchecked.PRESENT; + +/** + * An array {@link MqlValue value} in the context of the MongoDB Query + * Language (MQL). An array is a finite, ordered collection of elements of a + * certain type. It is also known as a finite mathematical sequence. + * + * @param the type of the elements + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlArray extends MqlValue { + + /** + * An array consisting of only those elements in {@code this} array that + * match the provided predicate. + * + * @param predicate the predicate to apply to each element to determine if + * it should be included. + * @return the resulting array. + */ + MqlArray filter(Function predicate); + + /** + * An array consisting of the results of applying the provided function to + * the elements of {@code this} array. + * + * @param in the function to apply to each element. + * @return the resulting array. + * @param the type of the elements of the resulting array. + */ + MqlArray map(Function in); + + /** + * The size of {@code this} array. + * + * @return the size. + */ + MqlInteger size(); + + /** + * Whether any value in {@code this} array satisfies the predicate. + * + * @param predicate the predicate. + * @return the resulting value. + */ + MqlBoolean any(Function predicate); + + /** + * Whether all values in {@code this} array satisfy the predicate. + * + * @param predicate the predicate. + * @return the resulting value. + */ + MqlBoolean all(Function predicate); + + /** + * The sum of adding together all the values of {@code this} array, + * via the provided {@code mapper}. Returns 0 if the array is empty. + * + *

    The mapper may be used to transform the values of {@code this} array + * into {@linkplain MqlNumber numbers}. If no transformation is + * necessary, then the identity function {@code array.sum(v -> v)} should + * be used. + * + * @param mapper the mapper function. + * @return the resulting value. + */ + MqlNumber sum(Function mapper); + + /** + * The product of multiplying together all the values of {@code this} array, + * via the provided {@code mapper}. Returns 1 if the array is empty. + * + *

    The mapper may be used to transform the values of {@code this} array + * into {@linkplain MqlNumber numbers}. If no transformation is + * necessary, then the identity function {@code array.multiply(v -> v)} + * should be used. + * + * @param mapper the mapper function. + * @return the resulting value. + */ + MqlNumber multiply(Function mapper); + + /** + * The {@linkplain #gt(MqlValue) largest} value all the values of + * {@code this} array, or the {@code other} value if this array is empty. + * + * @mongodb.server.release 5.2 + * @param other the other value. + * @return the resulting value. + */ + T max(T other); + + /** + * The {@linkplain #lt(MqlValue) smallest} value all the values of + * {@code this} array, or the {@code other} value if this array is empty. + * + * @mongodb.server.release 5.2 + * @param other the other value. + * @return the resulting value. + */ + T min(T other); + + /** + * The {@linkplain #gt(MqlValue) largest} {@code n} elements of + * {@code this} array, or all elements if the array contains fewer than + * {@code n} elements. + * + * @mongodb.server.release 5.2 + * @param n the number of elements. + * @return the resulting value. + */ + MqlArray maxN(MqlInteger n); + + /** + * The {@linkplain #lt(MqlValue) smallest} {@code n} elements of + * {@code this} array, or all elements if the array contains fewer than + * {@code n} elements. + * + * @mongodb.server.release 5.2 + * @param n the number of elements. + * @return the resulting value. + */ + MqlArray minN(MqlInteger n); + + /** + * The string-concatenation of all the values of {@code this} array, + * via the provided {@code mapper}. Returns the empty string if the array + * is empty. + * + *

    The mapper may be used to transform the values of {@code this} array + * into {@linkplain MqlString strings}. If no transformation is + * necessary, then the identity function {@code array.join(v -> v)} should + * be used. + * + * @param mapper the mapper function. + * @return the resulting value. + */ + MqlString joinStrings(Function mapper); + + /** + * The {@linkplain #concat(MqlArray) array-concatenation} + * of all the array values of {@code this} array, + * via the provided {@code mapper}. Returns the empty array if the array + * is empty. + * + *

    The mapper may be used to transform the values of {@code this} array + * into {@linkplain MqlArray arrays}. If no transformation is + * necessary, then the identity function {@code array.concat(v -> v)} should + * be used. + * + * @param mapper the mapper function. + * @return the resulting value. + * @param the type of the elements of the array. + */ + MqlArray concatArrays(Function> mapper); + + /** + * The {@linkplain #union(MqlArray) set-union} + * of all the array values of {@code this} array, + * via the provided {@code mapper}. Returns the empty array if the array + * is empty. + * + *

    The mapper may be used to transform the values of {@code this} array + * into {@linkplain MqlArray arrays}. If no transformation is + * necessary, then the identity function {@code array.union(v -> v)} should + * be used. + * + * @param mapper the mapper function. + * @return the resulting value. + * @param the type of the elements of the array. + */ + MqlArray unionArrays(Function> mapper); + + /** + * The {@linkplain MqlMap map} value corresponding to the + * {@linkplain MqlEntry entry} values of {@code this} array, + * via the provided {@code mapper}. Returns the empty map if the array + * is empty. + * + *

    The mapper may be used to transform the values of {@code this} array + * into {@linkplain MqlEntry entries}. If no transformation is + * necessary, then the identity function {@code array.union(v -> v)} should + * be used. + * + * @see MqlMap#entries() + * @param mapper the mapper function. + * @return the resulting value. + * @param the type of the resulting map's values. + */ + MqlMap asMap(Function> mapper); + + /** + * Returns the element at the provided index {@code i} for + * {@code this} array. + * + *

    Warning: The use of this method is an assertion that + * the index {@code i} is in bounds for the array. + * If the index is out of bounds for this array, then + * the behaviour of the API is not specified. + * + * @param i the index. + * @return the resulting value. + */ + @MqlUnchecked(PRESENT) + T elementAt(MqlInteger i); + + /** + * Returns the element at the provided index {@code i} for + * {@code this} array. + * + *

    Warning: The use of this method is an assertion that + * the index {@code i} is in bounds for the array. + * If the index is out of bounds for this array, then + * the behaviour of the API is not specified. + * + * @param i the index. + * @return the resulting value. + */ + @MqlUnchecked(PRESENT) + default T elementAt(final int i) { + return this.elementAt(of(i)); + } + + /** + * Returns the first element of {@code this} array. + * + *

    Warning: The use of this method is an assertion that + * the array is not empty. + * If the array is empty then the behaviour of the API is not specified. + * + * @mongodb.server.release 4.4 + * @return the resulting value. + */ + @MqlUnchecked(PRESENT) + T first(); + + /** + * Returns the last element of {@code this} array. + * + *

    Warning: The use of this method is an assertion that + * the array is not empty. + * If the array is empty then the behaviour of the API is not specified. + * + * @mongodb.server.release 4.4 + * @return the resulting value. + */ + @MqlUnchecked(PRESENT) + T last(); + + /** + * Whether {@code this} array contains a value that is + * {@linkplain #eq equal} to the provided {@code value}. + * + * @param value the value. + * @return the resulting value. + */ + MqlBoolean contains(T value); + + /** + * The result of concatenating {@code this} array first with + * the {@code other} array ensuing. + * + * @param other the other array. + * @return the resulting array. + */ + MqlArray concat(MqlArray other); + + /** + * The subarray of {@code this} array, from the {@code start} index + * inclusive, and continuing for the specified {@code length}, up to + * the end of the array. + * + * @param start start index + * @param length length + * @return the resulting value + */ + MqlArray slice(MqlInteger start, MqlInteger length); + + /** + * The subarray of {@code this} array, from the {@code start} index + * inclusive, and continuing for the specified {@code length}, or + * to the end of the array. + * + * @param start start index + * @param length length + * @return the resulting value + */ + default MqlArray slice(final int start, final int length) { + return this.slice(of(start), of(length)); + } + + /** + * The set-union of {@code this} array and the {@code other} array ensuing, + * containing only the distinct values of both. + * No guarantee is made regarding order. + * + * @param other the other array. + * @return the resulting array. + */ + MqlArray union(MqlArray other); + + + /** + * An array containing only the distinct values of {@code this} array. + * No guarantee is made regarding order. + * + * @return the resulting value + */ + MqlArray distinct(); + + /** + * The result of passing {@code this} value to the provided function. + * Equivalent to {@code f.apply(this)}, and allows lambdas and static, + * user-defined functions to use the chaining syntax. + * + * @see MqlValue#passTo + * @param f the function to apply. + * @return the resulting value. + * @param the type of the resulting value. + */ + R passArrayTo(Function, ? extends R> f); + + /** + * The result of applying the provided switch mapping to {@code this} value. + * + * @see MqlValue#switchOn + * @param mapping the switch mapping. + * @return the resulting value. + * @param the type of the resulting value. + */ + R switchArrayOn(Function>, ? extends BranchesTerminal, ? extends R>> mapping); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlBoolean.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlBoolean.java new file mode 100644 index 00000000000..5e594a757c7 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlBoolean.java @@ -0,0 +1,89 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; + +import java.util.function.Function; + +/** + * A boolean {@linkplain MqlValue value} in the context of the + * MongoDB Query Language (MQL). + * + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlBoolean extends MqlValue { + + /** + * The logical negation of {@code this} value. + * + * @return the resulting value. + */ + MqlBoolean not(); + + /** + * The logical conjunction of {@code this} and the {@code other} value. + * + * @param other the other boolean value. + * @return the resulting value. + */ + MqlBoolean or(MqlBoolean other); + + /** + * The logical disjunction of {@code this} and the {@code other} value. + * + * @param other the other boolean value. + * @return the resulting value. + */ + MqlBoolean and(MqlBoolean other); + + /** + * The {@code ifTrue} value when {@code this} is true, + * and the {@code ifFalse} value otherwise. + * + * @param ifTrue the ifTrue value. + * @param ifFalse the ifFalse value. + * @return the resulting value. + * @param The type of the resulting value. + */ + T cond(T ifTrue, T ifFalse); + + /** + * The result of passing {@code this} value to the provided function. + * Equivalent to {@code f.apply(this)}, and allows lambdas and static, + * user-defined functions to use the chaining syntax. + * + * @see MqlValue#passTo + * @param f the function to apply. + * @return the resulting value. + * @param the type of the resulting value. + */ + R passBooleanTo(Function f); + + /** + * The result of applying the provided switch mapping to {@code this} value. + * + * @see MqlValue#switchOn + * @param mapping the switch mapping. + * @return the resulting value. + * @param the type of the resulting value. + */ + R switchBooleanOn(Function, ? extends BranchesTerminal> mapping); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlDate.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlDate.java new file mode 100644 index 00000000000..7c39057ee23 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlDate.java @@ -0,0 +1,161 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; + +import java.util.function.Function; + +/** + * A UTC date-time {@linkplain MqlValue value} in the context + * of the MongoDB Query Language (MQL). Tracks the number of + * milliseconds since the Unix epoch, and does not track the timezone. + * + * @mongodb.driver.manual reference/operator/aggregation/dateToString/ Format Specifiers, UTC Offset, and Olson Timezone Identifier + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlDate extends MqlValue { + + /** + * The year of {@code this} date as determined by the provided + * {@code timezone}. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger year(MqlString timezone); + + /** + * The month of {@code this} date as determined by the provided + * {@code timezone}, as an integer between 1 and 12. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger month(MqlString timezone); + + /** + * The day of the month of {@code this} date as determined by the provided + * {@code timezone}, as an integer between 1 and 31. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger dayOfMonth(MqlString timezone); + + /** + * The day of the week of {@code this} date as determined by the provided + * {@code timezone}, as an integer between 1 (Sunday) and 7 (Saturday). + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger dayOfWeek(MqlString timezone); + + /** + * The day of the year of {@code this} date as determined by the provided + * {@code timezone}, as an integer between 1 and 366. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger dayOfYear(MqlString timezone); + + /** + * The hour of {@code this} date as determined by the provided + * {@code timezone}, as an integer between 0 and 23. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger hour(MqlString timezone); + + /** + * The minute of {@code this} date as determined by the provided + * {@code timezone}, as an integer between 0 and 59. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger minute(MqlString timezone); + + /** + * The second of {@code this} date as determined by the provided + * {@code timezone}, as an integer between 0 and 59, and 60 in the case + * of a leap second. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger second(MqlString timezone); + + /** + * The week of the year of {@code this} date as determined by the provided + * {@code timezone}, as an integer between 0 and 53. + * + *

    Weeks begin on Sundays, and week 1 begins with the first Sunday of the + * year. Days preceding the first Sunday of the year are in week 0. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger week(MqlString timezone); + + /** + * The millisecond part of {@code this} date as determined by the provided + * {@code timezone}, as an integer between 0 and 999. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlInteger millisecond(MqlString timezone); + + /** + * The string representation of {@code this} date as determined by the + * provided {@code timezone}, and formatted according to the {@code format}. + * + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @param format the format specifier. + * @return the resulting value. + */ + MqlString asString(MqlString timezone, MqlString format); + + /** + * The result of passing {@code this} value to the provided function. + * Equivalent to {@code f.apply(this)}, and allows lambdas and static, + * user-defined functions to use the chaining syntax. + * + * @see MqlValue#passTo + * @param f the function to apply. + * @return the resulting value. + * @param the type of the resulting value. + */ + R passDateTo(Function f); + + /** + * The result of applying the provided switch mapping to {@code this} value. + * + * @see MqlValue#switchOn + * @param mapping the switch mapping. + * @return the resulting value. + * @param the type of the resulting value. + */ + R switchDateOn(Function, ? extends BranchesTerminal> mapping); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlDocument.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlDocument.java new file mode 100644 index 00000000000..b99d5b3354b --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlDocument.java @@ -0,0 +1,547 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; +import com.mongodb.assertions.Assertions; +import org.bson.conversions.Bson; + +import java.time.Instant; +import java.util.function.Function; + +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofMap; +import static com.mongodb.client.model.mql.MqlUnchecked.Unchecked.PRESENT; +import static com.mongodb.client.model.mql.MqlUnchecked.Unchecked.TYPE; +import static com.mongodb.client.model.mql.MqlUnchecked.Unchecked.TYPE_ARGUMENT; + +/** + * A document {@link MqlValue value} in the context of the MongoDB Query + * Language (MQL). A document is a finite set of fields, where the field + * name is a string, together with a value of any other + * {@linkplain MqlValue type in the type hierarchy}. + * No field name is repeated. + * + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlDocument extends MqlValue { + + /** + * Whether {@code this} document has a field with the provided + * {@code fieldName} (if a field is set to null, it is present). + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + */ + MqlBoolean hasField(String fieldName); + + /** + * Returns a document with the same fields as {@code this} document, but + * with the {@code fieldName} field set to the specified {@code value}. + * + *

    This does not affect the original document. + * + *

    Warning: Users should take care to assign values, such that the types + * of those values correspond to the types of ensuing {@code get...} + * invocations, since this API has no way of verifying this correspondence. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param value the value. + * @return the resulting document. + */ + MqlDocument setField(String fieldName, MqlValue value); + + /** + * Returns a document with the same fields as {@code this} document, but + * excluding the field with the specified {@code fieldName}. + * + *

    This does not affect the original document. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting document. + */ + MqlDocument unsetField(String fieldName); + + /** + * Returns the {@linkplain MqlValue} value of the field + * with the provided {@code fieldName}. + * + *

    Warning: Use of this method is an assertion that the document + * {@linkplain #hasField(String) has} the named field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + */ + @MqlUnchecked(PRESENT) + MqlValue getField(String fieldName); + + /** + * Returns the {@linkplain MqlBoolean boolean} value of the field + * with the provided {@code fieldName}. + * + *

    Warning: The type and presence of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the document + * {@linkplain #hasField(String) has} the named field and + * the field value is of the specified type. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + */ + @MqlUnchecked({PRESENT, TYPE}) + MqlBoolean getBoolean(String fieldName); + + /** + * Returns the {@linkplain MqlBoolean boolean} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not a boolean + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + MqlBoolean getBoolean(String fieldName, MqlBoolean other); + + /** + * Returns the {@linkplain MqlBoolean boolean} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not a boolean + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + default MqlBoolean getBoolean(final String fieldName, final boolean other) { + Assertions.notNull("fieldName", fieldName); + return getBoolean(fieldName, of(other)); + } + + /** + * Returns the {@linkplain MqlNumber number} value of the field + * with the provided {@code fieldName}. + * + *

    Warning: The type and presence of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the document + * {@linkplain #hasField(String) has} the named field and + * the field value is of the specified type. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + */ + @MqlUnchecked({PRESENT, TYPE}) + MqlNumber getNumber(String fieldName); + + /** + * Returns the {@linkplain MqlNumber number} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not a number + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + MqlNumber getNumber(String fieldName, MqlNumber other); + + /** + * Returns the {@linkplain MqlNumber number} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not a number + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + default MqlNumber getNumber(final String fieldName, final Number other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getNumber(fieldName, MqlValues.numberToMqlNumber(other)); + } + + /** + * Returns the {@linkplain MqlInteger integer} value of the field + * with the provided {@code fieldName}. + * + *

    Warning: The type and presence of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the document + * {@linkplain #hasField(String) has} the named field and + * the field value is of the specified type. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + */ + @MqlUnchecked({PRESENT, TYPE}) + MqlInteger getInteger(String fieldName); + + /** + * Returns the {@linkplain MqlInteger integer} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not an integer + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + MqlInteger getInteger(String fieldName, MqlInteger other); + + /** + * Returns the {@linkplain MqlInteger integer} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not an integer + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + default MqlInteger getInteger(final String fieldName, final int other) { + Assertions.notNull("fieldName", fieldName); + return getInteger(fieldName, of(other)); + } + + /** + * Returns the {@linkplain MqlInteger integer} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not an integer + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + default MqlInteger getInteger(final String fieldName, final long other) { + Assertions.notNull("fieldName", fieldName); + return getInteger(fieldName, of(other)); + } + + /** + * Returns the {@linkplain MqlString string} value of the field + * with the provided {@code fieldName}. + * + *

    Warning: The type and presence of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the document + * {@linkplain #hasField(String) has} the named field and + * the field value is of the specified type. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + */ + @MqlUnchecked({PRESENT, TYPE}) + MqlString getString(String fieldName); + + /** + * Returns the {@linkplain MqlString string} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not a string + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + MqlString getString(String fieldName, MqlString other); + + /** + * Returns the {@linkplain MqlString string} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not a string + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + default MqlString getString(final String fieldName, final String other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getString(fieldName, of(other)); + } + + /** + * Returns the {@linkplain MqlDate date} value of the field + * with the provided {@code fieldName}. + * + *

    Warning: The type and presence of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the document + * {@linkplain #hasField(String) has} the named field and + * the field value is of the specified type. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + */ + @MqlUnchecked({PRESENT, TYPE}) + MqlDate getDate(String fieldName); + + /** + * Returns the {@linkplain MqlDate date} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not a date + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + MqlDate getDate(String fieldName, MqlDate other); + + /** + * Returns the {@linkplain MqlDate date} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not a date + * or if the document {@linkplain #hasField} no such field. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + default MqlDate getDate(final String fieldName, final Instant other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getDate(fieldName, of(other)); + } + + /** + * Returns the {@linkplain MqlDocument document} value of the field + * with the provided {@code fieldName}. + * + *

    Warning: The type and presence of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the document + * {@linkplain #hasField(String) has} the named field and + * the field value is of the specified type. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + */ + @MqlUnchecked({PRESENT, TYPE}) + MqlDocument getDocument(String fieldName); + + /** + * Returns the {@linkplain MqlDocument document} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value + * if the document {@linkplain #hasField} no such field, + * or if the specified field is not a (child) document + * (or other {@linkplain MqlValue#isDocumentOr document-like value}. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + MqlDocument getDocument(String fieldName, MqlDocument other); + + /** + * Returns the {@linkplain MqlDocument document} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value + * if the document {@linkplain #hasField} no such field, + * or if the specified field is not a (child) document + * (or other {@linkplain MqlValue#isDocumentOr document-like value}. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + */ + default MqlDocument getDocument(final String fieldName, final Bson other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getDocument(fieldName, of(other)); + } + + /** + * Returns the {@linkplain MqlMap map} value of the field + * with the provided {@code fieldName}. + * + *

    Warning: The type and presence of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the document + * {@linkplain #hasField(String) has} the named field, + * and the field value is of the specified raw type, + * and the field value's type has the specified type argument. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + * @param the type. + */ + @MqlUnchecked({PRESENT, TYPE}) + MqlMap<@MqlUnchecked(TYPE_ARGUMENT) T> getMap(String fieldName); + + + /** + * Returns the {@linkplain MqlMap map} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value + * if the document {@linkplain #hasField} no such field, + * or if the specified field is not a map + * (or other {@linkplain MqlValue#isMapOr} map-like value}). + * + *

    Warning: The type argument of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the type argument is correct. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + * @param the type. + */ + MqlMap getMap(String fieldName, MqlMap<@MqlUnchecked(TYPE_ARGUMENT) ? extends T> other); + + /** + * Returns the {@linkplain MqlMap map} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value + * if the document {@linkplain #hasField} no such field, + * or if the specified field is not a map + * (or other {@linkplain MqlValue#isMapOr} map-like value}). + * + *

    Warning: The type argument of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the type argument is correct. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + * @param the type. + */ + default MqlMap<@MqlUnchecked(TYPE_ARGUMENT) T> getMap(final String fieldName, final Bson other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getMap(fieldName, ofMap(other)); + } + + /** + * Returns the {@linkplain MqlArray array} value of the field + * with the provided {@code fieldName}. + * + *

    Warning: The type and presence of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the document + * {@linkplain #hasField(String) has} the named field, + * and the field value is of the specified raw type, + * and the field value's type has the specified type argument. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @return the resulting value. + * @param the type. + */ + @MqlUnchecked({PRESENT, TYPE}) + MqlArray<@MqlUnchecked(TYPE_ARGUMENT) T> getArray(String fieldName); + + /** + * Returns the {@linkplain MqlArray array} value of the field + * with the provided {@code fieldName}, + * or the {@code other} value if the field is not an array + * or if the document {@linkplain #hasField} no such field. + * + *

    Warning: The type argument of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the type argument is correct. + * + * @mongodb.server.release 5.0 + * @param fieldName the name of the field. + * @param other the other value. + * @return the resulting value. + * @param the type. + */ + MqlArray<@MqlUnchecked(TYPE_ARGUMENT) T> getArray(String fieldName, MqlArray other); + + /** + * Returns a document with the same fields as {@code this} document, but + * with any fields present in the {@code other} document overwritten with + * the fields of that other document. That is, fields from both this and the + * other document are merged, with the other document having priority. + * + *

    This does not affect the original document. + * + * @param other the other document. + * @return the resulting value. + */ + MqlDocument merge(MqlDocument other); + + /** + * {@code this} document as a {@linkplain MqlMap map}. + * + *

    Warning: The type argument of the resulting value is not + * enforced by the API. The use of this method is an + * unchecked assertion that the type argument is correct. + * + * @return the resulting value. + * @param the type. + */ + + MqlMap<@MqlUnchecked(TYPE_ARGUMENT) T> asMap(); + + /** + * The result of passing {@code this} value to the provided function. + * Equivalent to {@code f.apply(this)}, and allows lambdas and static, + * user-defined functions to use the chaining syntax. + * + * @see MqlValue#passTo + * @param f the function to apply. + * @return the resulting value. + * @param the type of the resulting value. + */ + R passDocumentTo(Function f); + + /** + * The result of applying the provided switch mapping to {@code this} value. + * + * @see MqlValue#switchOn + * @param mapping the switch mapping. + * @return the resulting value. + * @param the type of the resulting value. + */ + R switchDocumentOn(Function, ? extends BranchesTerminal> mapping); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlEntry.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlEntry.java new file mode 100644 index 00000000000..bcb1f26e251 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlEntry.java @@ -0,0 +1,75 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; + +/** + * A map entry {@linkplain MqlValue value} in the context + * of the MongoDB Query Language (MQL). An entry has a + * {@linkplain MqlString string} key and some + * {@linkplain MqlValue value}. Entries are used with + * {@linkplain MqlMap maps}. + * + *

    Entries are {@linkplain MqlValue#isDocumentOr document-like} and + * {@linkplain MqlValue#isMapOr map-like}, unless the method returning the + * entry specifies otherwise. + * + * @param The type of the value + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlEntry extends MqlValue { + + /** + * The key of {@code this} entry. + * + * @mongodb.server.release 5.0 + * @return the key. + */ + MqlString getKey(); + + /** + * The value of {@code this} entry. + * + * @mongodb.server.release 5.0 + * @return the value. + */ + T getValue(); + + /** + * An entry with the same key as {@code this} entry, and the + * specified {@code value}. + * + * @mongodb.server.release 5.0 + * @param value the value. + * @return the resulting entry. + */ + MqlEntry setValue(T value); + + /** + * An entry with the same value as {@code this} entry, and the + * specified {@code key}. + * + * @mongodb.server.release 5.0 + * @param key the key. + * @return the resulting entry. + */ + MqlEntry setKey(MqlString key); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlExpression.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlExpression.java new file mode 100644 index 00000000000..eb7ea9a68cd --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlExpression.java @@ -0,0 +1,1110 @@ +/* + * 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.mql; + +import com.mongodb.assertions.Assertions; +import org.bson.BsonArray; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonString; +import org.bson.BsonValue; +import org.bson.codecs.configuration.CodecRegistry; + +import java.util.Collections; +import java.util.function.BinaryOperator; +import java.util.function.Function; + +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofNull; +import static com.mongodb.client.model.mql.MqlValues.ofStringArray; + +final class MqlExpression + implements MqlValue, MqlBoolean, MqlInteger, MqlNumber, + MqlString, MqlDate, MqlDocument, MqlArray, MqlMap, MqlEntry { + + private final Function fn; + + MqlExpression(final Function fn) { + this.fn = fn; + } + + /** + * Exposes the evaluated BsonValue so that this mql expression may be used + * in aggregations. Non-public, as it is intended to be used only by the + * {@link MqlExpressionCodec}. + */ + BsonValue toBsonValue(final CodecRegistry codecRegistry) { + return fn.apply(codecRegistry).bsonValue; + } + + private AstPlaceholder astDoc(final String name, final BsonDocument value) { + return new AstPlaceholder(new BsonDocument(name, value)); + } + + @Override + public MqlString getKey() { + return new MqlExpression<>(getFieldInternal("k")); + } + + @Override + public T getValue() { + return newMqlExpression(getFieldInternal("v")); + } + + @Override + public MqlEntry setValue(final T value) { + Assertions.notNull("value", value); + return setFieldInternal("v", value); + } + + @Override + public MqlEntry setKey(final MqlString key) { + Assertions.notNull("key", key); + return setFieldInternal("k", key); + } + + static final class AstPlaceholder { + private final BsonValue bsonValue; + + AstPlaceholder(final BsonValue bsonValue) { + this.bsonValue = bsonValue; + } + } + + private Function ast(final String name) { + return (cr) -> new AstPlaceholder(new BsonDocument(name, this.toBsonValue(cr))); + } + + // in cases where we must wrap the first argument in an array + private Function astWrapped(final String name) { + return (cr) -> new AstPlaceholder(new BsonDocument(name, + new BsonArray(Collections.singletonList(this.toBsonValue(cr))))); + } + + private Function ast(final String name, final MqlValue param1) { + return (cr) -> { + BsonArray value = new BsonArray(); + value.add(this.toBsonValue(cr)); + value.add(toBsonValue(cr, param1)); + return new AstPlaceholder(new BsonDocument(name, value)); + }; + } + + private Function ast(final String name, final MqlValue param1, final MqlValue param2) { + return (cr) -> { + BsonArray value = new BsonArray(); + value.add(this.toBsonValue(cr)); + value.add(toBsonValue(cr, param1)); + value.add(toBsonValue(cr, param2)); + return new AstPlaceholder(new BsonDocument(name, value)); + }; + } + + /** + * Takes an expression and converts it to a BsonValue. MqlExpression will be + * the only implementation of Expression and all subclasses, so this will + * not mis-cast an expression as anything else. + */ + static BsonValue toBsonValue(final CodecRegistry cr, final MqlValue mqlValue) { + return ((MqlExpression) mqlValue).toBsonValue(cr); + } + + /** + * Converts an MqlExpression to any subtype of Expression. Users must not + * extend Expression or its subtypes, so MqlExpression will implement any R. + */ + @SuppressWarnings("unchecked") + R assertImplementsAllExpressions() { + return (R) this; + } + + private static R newMqlExpression(final Function ast) { + return new MqlExpression<>(ast).assertImplementsAllExpressions(); + } + + private R variable(final String variable) { + return newMqlExpression((cr) -> new AstPlaceholder(new BsonString(variable))); + } + + /** @see MqlBoolean */ + + @Override + public MqlBoolean not() { + return new MqlExpression<>(ast("$not")); + } + + @Override + public MqlBoolean or(final MqlBoolean other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$or", other)); + } + + @Override + public MqlBoolean and(final MqlBoolean other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$and", other)); + } + + @Override + public R cond(final R ifTrue, final R ifFalse) { + Assertions.notNull("ifTrue", ifTrue); + Assertions.notNull("ifFalse", ifFalse); + return newMqlExpression(ast("$cond", ifTrue, ifFalse)); + } + + /** @see MqlDocument */ + + 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 MqlValue getField(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public MqlBoolean getBoolean(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public MqlBoolean getBoolean(final String fieldName, final MqlBoolean other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getBoolean(fieldName).isBooleanOr(other); + } + + @Override + public MqlNumber getNumber(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public MqlNumber getNumber(final String fieldName, final MqlNumber other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getNumber(fieldName).isNumberOr(other); + } + + @Override + public MqlInteger getInteger(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public MqlInteger getInteger(final String fieldName, final MqlInteger other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getInteger(fieldName).isIntegerOr(other); + } + + @Override + public MqlString getString(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public MqlString getString(final String fieldName, final MqlString other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getString(fieldName).isStringOr(other); + } + + @Override + public MqlDate getDate(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public MqlDate getDate(final String fieldName, final MqlDate other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getDate(fieldName).isDateOr(other); + } + + @Override + public MqlDocument getDocument(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public MqlMap getMap(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public MqlMap getMap(final String fieldName, final MqlMap other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getMap(fieldName).isMapOr(other); + } + + @Override + public MqlDocument getDocument(final String fieldName, final MqlDocument other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getDocument(fieldName).isDocumentOr(other); + } + + @Override + public MqlArray getArray(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return new MqlExpression<>(getFieldInternal(fieldName)); + } + + @Override + public MqlArray getArray(final String fieldName, final MqlArray other) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("other", other); + return getArray(fieldName).isArrayOr(other); + } + + @Override + public MqlDocument merge(final MqlDocument other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$mergeObjects", other)); + } + + @Override + public MqlDocument setField(final String fieldName, final MqlValue value) { + Assertions.notNull("fieldName", fieldName); + Assertions.notNull("value", value); + return setFieldInternal(fieldName, value); + } + + private MqlExpression setFieldInternal(final String fieldName, final MqlValue value) { + Assertions.notNull("fieldName", fieldName); + return newMqlExpression((cr) -> astDoc("$setField", new BsonDocument() + .append("field", new BsonString(fieldName)) + .append("input", this.toBsonValue(cr)) + .append("value", toBsonValue(cr, value)))); + } + + @Override + public MqlDocument unsetField(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return newMqlExpression((cr) -> astDoc("$unsetField", new BsonDocument() + .append("field", new BsonString(fieldName)) + .append("input", this.toBsonValue(cr)))); + } + + /** @see MqlValue */ + + @Override + public R passTo(final Function f) { + Assertions.notNull("f", f); + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchOn(final Function, ? extends BranchesTerminal> mapping) { + Assertions.notNull("mapping", mapping); + return switchMapInternal(this.assertImplementsAllExpressions(), mapping.apply(new Branches<>())); + } + + @Override + public R passBooleanTo(final Function f) { + Assertions.notNull("f", f); + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchBooleanOn(final Function, ? extends BranchesTerminal> mapping) { + Assertions.notNull("mapping", mapping); + return switchMapInternal(this.assertImplementsAllExpressions(), mapping.apply(new Branches<>())); + } + + @Override + public R passIntegerTo(final Function f) { + Assertions.notNull("f", f); + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchIntegerOn(final Function, ? extends BranchesTerminal> mapping) { + Assertions.notNull("mapping", mapping); + return switchMapInternal(this.assertImplementsAllExpressions(), mapping.apply(new Branches<>())); + } + + @Override + public R passNumberTo(final Function f) { + Assertions.notNull("f", f); + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchNumberOn(final Function, ? extends BranchesTerminal> mapping) { + Assertions.notNull("mapping", mapping); + return switchMapInternal(this.assertImplementsAllExpressions(), mapping.apply(new Branches<>())); + } + + @Override + public R passStringTo(final Function f) { + Assertions.notNull("f", f); + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchStringOn(final Function, ? extends BranchesTerminal> mapping) { + Assertions.notNull("mapping", mapping); + return switchMapInternal(this.assertImplementsAllExpressions(), mapping.apply(new Branches<>())); + } + + @Override + public R passDateTo(final Function f) { + Assertions.notNull("f", f); + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchDateOn(final Function, ? extends BranchesTerminal> mapping) { + Assertions.notNull("mapping", mapping); + return switchMapInternal(this.assertImplementsAllExpressions(), mapping.apply(new Branches<>())); + } + + @Override + public R passArrayTo(final Function, ? extends R> f) { + Assertions.notNull("f", f); + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchArrayOn(final Function>, ? extends BranchesTerminal, ? extends R>> mapping) { + Assertions.notNull("mapping", mapping); + return switchMapInternal(this.assertImplementsAllExpressions(), mapping.apply(new Branches<>())); + } + + @Override + public R passMapTo(final Function, ? extends R> f) { + Assertions.notNull("f", f); + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchMapOn(final Function>, ? extends BranchesTerminal, ? extends R>> mapping) { + Assertions.notNull("mapping", mapping); + return switchMapInternal(this.assertImplementsAllExpressions(), mapping.apply(new Branches<>())); + } + + @Override + public R passDocumentTo(final Function f) { + Assertions.notNull("f", f); + return f.apply(this.assertImplementsAllExpressions()); + } + + @Override + public R switchDocumentOn(final Function, ? extends BranchesTerminal> mapping) { + Assertions.notNull("mapping", mapping); + return switchMapInternal(this.assertImplementsAllExpressions(), mapping.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", toBsonValue(cr, result.getCaseValue())) + .append("then", toBsonValue(cr, result.getThenValue()))); + } + BsonDocument switchBson = new BsonDocument().append("branches", branches); + if (construct.getDefaults() != null) { + switchBson = switchBson.append("default", toBsonValue(cr, construct.getDefaults().apply(value))); + } + return astDoc("$switch", switchBson); + }); + } + + @Override + public MqlBoolean eq(final MqlValue other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$eq", other)); + } + + @Override + public MqlBoolean ne(final MqlValue other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$ne", other)); + } + + @Override + public MqlBoolean gt(final MqlValue other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$gt", other)); + } + + @Override + public MqlBoolean gte(final MqlValue other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$gte", other)); + } + + @Override + public MqlBoolean lt(final MqlValue other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$lt", other)); + } + + @Override + public MqlBoolean lte(final MqlValue other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$lte", other)); + } + + MqlBoolean isBoolean() { + return new MqlExpression<>(astWrapped("$type")).eq(of("bool")); + } + + @Override + public MqlBoolean isBooleanOr(final MqlBoolean other) { + Assertions.notNull("other", other); + return this.isBoolean().cond(this, other); + } + + MqlBoolean isNumber() { + return new MqlExpression<>(astWrapped("$isNumber")); + } + + @Override + public MqlNumber isNumberOr(final MqlNumber other) { + Assertions.notNull("other", other); + return this.isNumber().cond(this, other); + } + + MqlBoolean isInteger() { + return switchOn(on -> on + .isNumber(v -> v.round().eq(v)) + .defaults(v -> of(false))); + } + + @Override + public MqlInteger isIntegerOr(final MqlInteger other) { + Assertions.notNull("other", 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 -> (MqlInteger) v.round().eq(v).cond(v, other)) + .defaults(v -> other)); + } + + MqlBoolean isString() { + return new MqlExpression<>(astWrapped("$type")).eq(of("string")); + } + + @Override + public MqlString isStringOr(final MqlString other) { + Assertions.notNull("other", other); + return this.isString().cond(this, other); + } + + MqlBoolean isDate() { + return ofStringArray("date").contains(new MqlExpression<>(astWrapped("$type"))); + } + + @Override + public MqlDate isDateOr(final MqlDate other) { + Assertions.notNull("other", other); + return this.isDate().cond(this, other); + } + + MqlBoolean isArray() { + return new MqlExpression<>(astWrapped("$isArray")); + } + + /** + * checks if array (but cannot check type) + * user asserts array is of type R + * + * @param other + * @return + * @param + */ + @SuppressWarnings("unchecked") + @Override + public MqlArray isArrayOr(final MqlArray other) { + Assertions.notNull("other", other); + return (MqlArray) this.isArray().cond(this.assertImplementsAllExpressions(), other); + } + + MqlBoolean isDocumentOrMap() { + return new MqlExpression<>(astWrapped("$type")).eq(of("object")); + } + + @Override + public R isDocumentOr(final R other) { + Assertions.notNull("other", other); + return this.isDocumentOrMap().cond(this.assertImplementsAllExpressions(), other); + } + + @Override + public MqlMap isMapOr(final MqlMap other) { + Assertions.notNull("other", other); + MqlExpression isMap = (MqlExpression) this.isDocumentOrMap(); + return newMqlExpression(isMap.ast("$cond", this.assertImplementsAllExpressions(), other)); + } + + MqlBoolean isNull() { + return this.eq(ofNull()); + } + + @Override + public MqlString asString() { + return new MqlExpression<>(astWrapped("$toString")); + } + + private Function convertInternal(final String to, final MqlValue other) { + return (cr) -> astDoc("$convert", new BsonDocument() + .append("input", this.fn.apply(cr).bsonValue) + .append("onError", toBsonValue(cr, other)) + .append("to", new BsonString(to))); + } + + @Override + public MqlInteger parseInteger() { + MqlValue asLong = new MqlExpression<>(ast("$toLong")); + return new MqlExpression<>(convertInternal("int", asLong)); + } + + /** @see MqlArray */ + + @Override + public MqlArray map(final Function in) { + Assertions.notNull("in", in); + T varThis = variable("$$this"); + return new MqlExpression<>((cr) -> astDoc("$map", new BsonDocument() + .append("input", this.toBsonValue(cr)) + .append("in", toBsonValue(cr, in.apply(varThis))))); + } + + @Override + public MqlArray filter(final Function predicate) { + Assertions.notNull("predicate", predicate); + T varThis = variable("$$this"); + return new MqlExpression<>((cr) -> astDoc("$filter", new BsonDocument() + .append("input", this.toBsonValue(cr)) + .append("cond", toBsonValue(cr, predicate.apply(varThis))))); + } + + MqlArray sort() { + return new MqlExpression<>((cr) -> astDoc("$sortArray", new BsonDocument() + .append("input", this.toBsonValue(cr)) + .append("sortBy", new BsonInt32(1)))); + } + + private T reduce(final T initialValue, final BinaryOperator in) { + T varThis = variable("$$this"); + T varValue = variable("$$value"); + return newMqlExpression((cr) -> astDoc("$reduce", new BsonDocument() + .append("input", this.toBsonValue(cr)) + .append("initialValue", toBsonValue(cr, initialValue)) + .append("in", toBsonValue(cr, in.apply(varValue, varThis))))); + } + + @Override + public MqlBoolean any(final Function predicate) { + Assertions.notNull("predicate", predicate); + MqlExpression array = (MqlExpression) this.map(predicate); + return array.reduce(of(false), (a, b) -> a.or(b)); + } + + @Override + public MqlBoolean all(final Function predicate) { + Assertions.notNull("predicate", predicate); + MqlExpression array = (MqlExpression) this.map(predicate); + return array.reduce(of(true), (a, b) -> a.and(b)); + } + + @SuppressWarnings("unchecked") + @Override + public MqlNumber sum(final Function mapper) { + Assertions.notNull("mapper", mapper); + // no sum that returns IntegerExpression, both have same erasure + MqlExpression array = (MqlExpression) this.map(mapper); + return array.reduce(of(0), (a, b) -> a.add(b)); + } + + @SuppressWarnings("unchecked") + @Override + public MqlNumber multiply(final Function mapper) { + Assertions.notNull("mapper", mapper); + MqlExpression array = (MqlExpression) this.map(mapper); + return array.reduce(of(1), (MqlNumber a, MqlNumber b) -> a.multiply(b)); + } + + @Override + public T max(final T other) { + Assertions.notNull("other", other); + return this.size().eq(of(0)).cond(other, this.maxN(of(1)).first()); + } + + @Override + public T min(final T other) { + Assertions.notNull("other", other); + return this.size().eq(of(0)).cond(other, this.minN(of(1)).first()); + } + + @Override + public MqlArray maxN(final MqlInteger n) { + Assertions.notNull("n", n); + return newMqlExpression((CodecRegistry cr) -> astDoc("$maxN", new BsonDocument() + .append("input", toBsonValue(cr, this)) + .append("n", toBsonValue(cr, n)))); + } + + @Override + public MqlArray minN(final MqlInteger n) { + Assertions.notNull("n", n); + return newMqlExpression((CodecRegistry cr) -> astDoc("$minN", new BsonDocument() + .append("input", toBsonValue(cr, this)) + .append("n", toBsonValue(cr, n)))); + } + + @Override + public MqlString joinStrings(final Function mapper) { + Assertions.notNull("mapper", mapper); + MqlExpression array = (MqlExpression) this.map(mapper); + return array.reduce(of(""), (a, b) -> a.append(b)); + } + + @SuppressWarnings("unchecked") + @Override + public MqlArray concatArrays(final Function> mapper) { + Assertions.notNull("mapper", mapper); + MqlExpression> array = (MqlExpression>) this.map(mapper); + return array.reduce(MqlValues.ofArray(), (a, b) -> a.concat(b)); + } + + @SuppressWarnings("unchecked") + @Override + public MqlArray unionArrays(final Function> mapper) { + Assertions.notNull("mapper", mapper); + Assertions.notNull("mapper", mapper); + MqlExpression> array = (MqlExpression>) this.map(mapper); + return array.reduce(MqlValues.ofArray(), (a, b) -> a.union(b)); + } + + @Override + public MqlInteger size() { + return new MqlExpression<>(astWrapped("$size")); + } + + @Override + public T elementAt(final MqlInteger i) { + Assertions.notNull("i", i); + return new MqlExpression<>(ast("$arrayElemAt", i)) + .assertImplementsAllExpressions(); + } + + @Override + public T first() { + return new MqlExpression<>(astWrapped("$first")) + .assertImplementsAllExpressions(); + } + + @Override + public T last() { + return new MqlExpression<>(astWrapped("$last")) + .assertImplementsAllExpressions(); + } + + @Override + public MqlBoolean contains(final T value) { + Assertions.notNull("value", value); + String name = "$in"; + return new MqlExpression<>((cr) -> { + BsonArray array = new BsonArray(); + array.add(toBsonValue(cr, value)); + array.add(this.toBsonValue(cr)); + return new AstPlaceholder(new BsonDocument(name, array)); + }).assertImplementsAllExpressions(); + } + + @Override + public MqlArray concat(final MqlArray other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$concatArrays", other)) + .assertImplementsAllExpressions(); + } + + @Override + public MqlArray slice(final MqlInteger start, final MqlInteger length) { + Assertions.notNull("start", start); + Assertions.notNull("length", length); + return new MqlExpression<>(ast("$slice", start, length)) + .assertImplementsAllExpressions(); + } + + @Override + public MqlArray union(final MqlArray other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$setUnion", other)) + .assertImplementsAllExpressions(); + } + + @Override + public MqlArray distinct() { + return new MqlExpression<>(astWrapped("$setUnion")); + } + + + /** @see MqlInteger + * @see MqlNumber */ + + @Override + public MqlInteger multiply(final MqlNumber other) { + Assertions.notNull("other", other); + return newMqlExpression(ast("$multiply", other)); + } + + @Override + public MqlNumber add(final MqlNumber other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$add", other)); + } + + @Override + public MqlNumber divide(final MqlNumber other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$divide", other)); + } + + @Override + public MqlNumber max(final MqlNumber other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$max", other)); + } + + @Override + public MqlNumber min(final MqlNumber other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$min", other)); + } + + @Override + public MqlInteger round() { + return new MqlExpression<>(ast("$round")); + } + + @Override + public MqlNumber round(final MqlInteger place) { + Assertions.notNull("place", place); + return new MqlExpression<>(ast("$round", place)); + } + + @Override + public MqlInteger multiply(final MqlInteger other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$multiply", other)); + } + + @Override + public MqlInteger abs() { + return newMqlExpression(ast("$abs")); + } + + @Override + public MqlDate millisecondsAsDate() { + return newMqlExpression(ast("$toDate")); + } + + @Override + public MqlNumber subtract(final MqlNumber other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$subtract", other)); + } + + @Override + public MqlInteger add(final MqlInteger other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$add", other)); + } + + @Override + public MqlInteger subtract(final MqlInteger other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$subtract", other)); + } + + @Override + public MqlInteger max(final MqlInteger other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$max", other)); + } + + @Override + public MqlInteger min(final MqlInteger other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$min", other)); + } + + /** @see MqlDate */ + + private MqlExpression usingTimezone(final String name, final MqlString timezone) { + return new MqlExpression<>((cr) -> astDoc(name, new BsonDocument() + .append("date", this.toBsonValue(cr)) + .append("timezone", toBsonValue(cr, timezone)))); + } + + @Override + public MqlInteger year(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$year", timezone); + } + + @Override + public MqlInteger month(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$month", timezone); + } + + @Override + public MqlInteger dayOfMonth(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$dayOfMonth", timezone); + } + + @Override + public MqlInteger dayOfWeek(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$dayOfWeek", timezone); + } + + @Override + public MqlInteger dayOfYear(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$dayOfYear", timezone); + } + + @Override + public MqlInteger hour(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$hour", timezone); + } + + @Override + public MqlInteger minute(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$minute", timezone); + } + + @Override + public MqlInteger second(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$second", timezone); + } + + @Override + public MqlInteger week(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$week", timezone); + } + + @Override + public MqlInteger millisecond(final MqlString timezone) { + Assertions.notNull("timezone", timezone); + return usingTimezone("$millisecond", timezone); + } + + @Override + public MqlString asString(final MqlString timezone, final MqlString format) { + Assertions.notNull("timezone", timezone); + Assertions.notNull("format", format); + return newMqlExpression((cr) -> astDoc("$dateToString", new BsonDocument() + .append("date", this.toBsonValue(cr)) + .append("format", toBsonValue(cr, format)) + .append("timezone", toBsonValue(cr, timezone)))); + } + + @Override + public MqlDate parseDate(final MqlString timezone, final MqlString format) { + Assertions.notNull("timezone", timezone); + Assertions.notNull("format", format); + return newMqlExpression((cr) -> astDoc("$dateFromString", new BsonDocument() + .append("dateString", this.toBsonValue(cr)) + .append("format", toBsonValue(cr, format)) + .append("timezone", toBsonValue(cr, timezone)))); + } + + @Override + public MqlDate parseDate(final MqlString format) { + Assertions.notNull("format", format); + return newMqlExpression((cr) -> astDoc("$dateFromString", new BsonDocument() + .append("dateString", this.toBsonValue(cr)) + .append("format", toBsonValue(cr, format)))); + } + + @Override + public MqlDate parseDate() { + return newMqlExpression((cr) -> astDoc("$dateFromString", new BsonDocument() + .append("dateString", this.toBsonValue(cr)))); + } + + /** @see MqlString */ + + @Override + public MqlString toLower() { + return new MqlExpression<>(ast("$toLower")); + } + + @Override + public MqlString toUpper() { + return new MqlExpression<>(ast("$toUpper")); + } + + @Override + public MqlString append(final MqlString other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$concat", other)); + } + + @Override + public MqlInteger length() { + return new MqlExpression<>(ast("$strLenCP")); + } + + @Override + public MqlInteger lengthBytes() { + return new MqlExpression<>(ast("$strLenBytes")); + } + + @Override + public MqlString substr(final MqlInteger start, final MqlInteger length) { + Assertions.notNull("start", start); + Assertions.notNull("length", length); + return new MqlExpression<>(ast("$substrCP", start, length)); + } + + @Override + public MqlString substrBytes(final MqlInteger start, final MqlInteger length) { + Assertions.notNull("start", start); + Assertions.notNull("length", length); + return new MqlExpression<>(ast("$substrBytes", start, length)); + } + + @Override + public MqlBoolean has(final MqlString key) { + Assertions.notNull("key", key); + return get(key).ne(ofRem()); + } + + + @Override + public MqlBoolean hasField(final String fieldName) { + Assertions.notNull("fieldName", fieldName); + return this.has(of(fieldName)); + } + + static R ofRem() { + // $$REMOVE is intentionally not exposed to users + return new MqlExpression<>((cr) -> new MqlExpression.AstPlaceholder(new BsonString("$$REMOVE"))) + .assertImplementsAllExpressions(); + } + + /** @see MqlMap + * @see MqlEntry */ + + @Override + public T get(final MqlString key) { + Assertions.notNull("key", key); + return newMqlExpression((cr) -> astDoc("$getField", new BsonDocument() + .append("input", this.fn.apply(cr).bsonValue) + .append("field", toBsonValue(cr, key)))); + } + + @SuppressWarnings("unchecked") + @Override + public T get(final MqlString key, final T other) { + Assertions.notNull("key", key); + Assertions.notNull("other", other); + MqlExpression mqlExpression = (MqlExpression) get(key); + return (T) mqlExpression.eq(ofRem()).cond(other, mqlExpression); + } + + @Override + public MqlMap set(final MqlString key, final T value) { + Assertions.notNull("key", key); + Assertions.notNull("value", value); + return newMqlExpression((cr) -> astDoc("$setField", new BsonDocument() + .append("field", toBsonValue(cr, key)) + .append("input", this.toBsonValue(cr)) + .append("value", toBsonValue(cr, value)))); + } + + @Override + public MqlMap unset(final MqlString key) { + Assertions.notNull("key", key); + return newMqlExpression((cr) -> astDoc("$unsetField", new BsonDocument() + .append("field", toBsonValue(cr, key)) + .append("input", this.toBsonValue(cr)))); + } + + @Override + public MqlMap merge(final MqlMap other) { + Assertions.notNull("other", other); + return new MqlExpression<>(ast("$mergeObjects", other)); + } + + @Override + public MqlArray> entries() { + return newMqlExpression(ast("$objectToArray")); + } + + @Override + public MqlMap asMap( + final Function> mapper) { + Assertions.notNull("mapper", mapper); + @SuppressWarnings("unchecked") + MqlExpression> array = (MqlExpression>) this.map(mapper); + return newMqlExpression(array.astWrapped("$arrayToObject")); + } + + @SuppressWarnings("unchecked") + @Override + public MqlMap asMap() { + return (MqlMap) this; + } + + @SuppressWarnings("unchecked") + @Override + public R asDocument() { + return (R) this; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlExpressionCodec.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlExpressionCodec.java new file mode 100644 index 00000000000..70f4329b6d0 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlExpressionCodec.java @@ -0,0 +1,52 @@ +/* + * 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.mql; + +import org.bson.BsonReader; +import org.bson.BsonValue; +import org.bson.BsonWriter; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.configuration.CodecRegistry; + +@SuppressWarnings("rawtypes") +final class MqlExpressionCodec implements Codec { + private final CodecRegistry codecRegistry; + + MqlExpressionCodec(final CodecRegistry codecRegistry) { + this.codecRegistry = codecRegistry; + } + + @Override + public MqlExpression decode(final BsonReader reader, final DecoderContext decoderContext) { + throw new UnsupportedOperationException("Decoding to an MqlExpression is not supported"); + } + + @Override + @SuppressWarnings({"unchecked"}) + public void encode(final BsonWriter writer, final MqlExpression value, final EncoderContext encoderContext) { + BsonValue bsonValue = value.toBsonValue(codecRegistry); + Codec codec = codecRegistry.get(bsonValue.getClass()); + codec.encode(writer, bsonValue, encoderContext); + } + + @Override + public Class getEncoderClass() { + return MqlExpression.class; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlInteger.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlInteger.java new file mode 100644 index 00000000000..0fe85fd88d9 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlInteger.java @@ -0,0 +1,145 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; + +import java.util.function.Function; + +/** + * An integer {@linkplain MqlValue value} in the context of the MongoDB Query + * Language (MQL). Integers are a subset of {@linkplain MqlNumber numbers}, + * and so, for example, the integer 0 and the number 0 are + * {@linkplain #eq(MqlValue) equal}. + * + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlInteger extends MqlNumber { + + /** + * The product of multiplying {@code this} and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlInteger multiply(MqlInteger other); + + /** + * The product of multiplying {@code this} and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + default MqlInteger multiply(final int other) { + return this.multiply(MqlValues.of(other)); + } + + /** + * The sum of adding {@code this} and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlInteger add(MqlInteger other); + + /** + * The sum of adding {@code this} and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + default MqlInteger add(final int other) { + return this.add(MqlValues.of(other)); + } + + /** + * The difference of subtracting the {@code other} value from {@code this}. + * + * @param other the other value. + * @return the resulting value. + */ + MqlInteger subtract(MqlInteger other); + + /** + * The difference of subtracting the {@code other} value from {@code this}. + * + * @param other the other value. + * @return the resulting value. + */ + default MqlInteger subtract(final int other) { + return this.subtract(MqlValues.of(other)); + } + + /** + * The {@linkplain #gt(MqlValue) larger} value of {@code this} + * and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlInteger max(MqlInteger other); + + /** + * The {@linkplain #lt(MqlValue) smaller} value of {@code this} + * and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlInteger min(MqlInteger other); + + /** + * The absolute value of {@code this} value. + * + * @return the resulting value. + */ + MqlInteger abs(); + + /** + * The {@linkplain MqlDate date} corresponding to {@code this} value + * when taken to be the number of milliseconds since the Unix epoch. + * + * @mongodb.server.release 4.0 + * @return the resulting value. + */ + MqlDate millisecondsAsDate(); + + /** + * The result of passing {@code this} value to the provided function. + * Equivalent to {@code f.apply(this)}, and allows lambdas and static, + * user-defined functions to use the chaining syntax. + * + * @see MqlValue#passTo + * @param f the function to apply. + * @return the resulting value. + * @param the type of the resulting value. + */ + R passIntegerTo(Function f); + + /** + * The result of applying the provided switch mapping to {@code this} value. + * + * @see MqlValue#switchOn + * @param mapping the switch mapping. + * @return the resulting value. + * @param the type of the resulting value. + */ + R switchIntegerOn(Function, ? extends BranchesTerminal> mapping); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlMap.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlMap.java new file mode 100644 index 00000000000..24ee3ef405b --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlMap.java @@ -0,0 +1,223 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; +import com.mongodb.assertions.Assertions; + +import java.util.function.Function; + +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlUnchecked.Unchecked.PRESENT; + +/** + * A map {@link MqlValue value} in the context of the MongoDB Query + * Language (MQL). A map is a finite set of + * {@link MqlEntry entries} of a certain type. + * No entry key is repeated. It is a mapping from keys to values. + * + * @param the type of the entry values + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlMap extends MqlValue { + + /** + * Whether {@code this} map has a value (including null) for + * the provided key. + * + * @param key the key. + * @return the resulting value. + */ + MqlBoolean has(MqlString key); + + /** + * Whether {@code this} map has a value (including null) for + * the provided key. + * + * @param key the key. + * @return the resulting value. + */ + default MqlBoolean has(final String key) { + Assertions.notNull("key", key); + return has(of(key)); + } + + /** + * The value corresponding to the provided key. + * + *

    Warning: The use of this method is an unchecked assertion that + * the key is present (which may be confirmed via {@link #has}). See + * {@link #get(MqlString, MqlValue)} for a typesafe variant. + * + * @param key the key. + * @return the value. + */ + @MqlUnchecked(PRESENT) + T get(MqlString key); + + /** + * The value corresponding to the provided key. + * + *

    Warning: The use of this method is an unchecked assertion that + * the key is present (which may be confirmed via {@link #has}). See + * {@link #get(MqlString, MqlValue)} for a typesafe variant. + * + * @param key the key. + * @return the value. + */ + @MqlUnchecked(PRESENT) + default T get(final String key) { + Assertions.notNull("key", key); + return get(of(key)); + } + + /** + * The value corresponding to the provided {@code key}, or the + * {@code other} value if an entry for the key is not + * {@linkplain #has(MqlString) present}. + * + * @param key the key. + * @param other the other value. + * @return the resulting value. + */ + T get(MqlString key, T other); + + /** + * The value corresponding to the provided {@code key}, or the + * {@code other} value if an entry for the key is not + * {@linkplain #has(MqlString) present}. + * + * @param key the key. + * @param other the other value. + * @return the resulting value. + */ + default T get(final String key, final T other) { + Assertions.notNull("key", key); + Assertions.notNull("other", other); + return get(of(key), other); + } + + /** + * Returns a map with the same entries as {@code this} map, but with + * the specified {@code key} set to the specified {@code value}. + * + *

    This does not affect the original map. + * + * @param key the key. + * @param value the value. + * @return the resulting value. + */ + MqlMap set(MqlString key, T value); + + /** + * Returns a map with the same entries as {@code this} map, but with + * the specified {@code key} set to the specified {@code value}. + * + *

    This does not affect the original map. + * + * @param key the key. + * @param value the value. + * @return the resulting value. + */ + default MqlMap set(final String key, final T value) { + Assertions.notNull("key", key); + Assertions.notNull("value", value); + return set(of(key), value); + } + + /** + * Returns a map with the same entries as {@code this} map, but which + * {@linkplain #has(MqlString) has} no entry with the specified + * {@code key}. + * + *

    This does not affect the original map. + * + * @param key the key. + * @return the resulting value. + */ + MqlMap unset(MqlString key); + + /** + * Returns a map with the same entries as {@code this} map, but which + * {@linkplain #has(MqlString) has} no entry with the specified + * {@code key}. + * + *

    This does not affect the original map. + * + * @param key the key. + * @return the resulting value. + */ + default MqlMap unset(final String key) { + Assertions.notNull("key", key); + return unset(of(key)); + } + + /** + * Returns a map with the same entries as {@code this} map, but with + * any keys present in the {@code other} map overwritten with the + * values of that other map. That is, entries from both this and the + * other map are merged, with the other map having priority. + * + *

    This does not affect the original map. + * + * @param other the other map. + * @return the resulting value. + */ + MqlMap merge(MqlMap other); + + /** + * The {@linkplain MqlEntry entries} of this map as an array. + * No guarantee is made regarding order. + * + * @see MqlArray#asMap + * @return the resulting value. + */ + MqlArray> entries(); + + /** + * {@code this} map as a {@linkplain MqlDocument document}. + * + * @return the resulting value. + * @param the resulting type. + */ + R asDocument(); + + /** + * The result of passing {@code this} value to the provided function. + * Equivalent to {@code f.apply(this)}, and allows lambdas and static, + * user-defined functions to use the chaining syntax. + * + * @see MqlValue#passTo + * @param f the function to apply. + * @return the resulting value. + * @param the type of the resulting value. + */ + R passMapTo(Function, ? extends R> f); + + /** + * The result of applying the provided switch mapping to {@code this} value. + * + * @see MqlValue#switchOn + * @param mapping the switch mapping. + * @return the resulting value. + * @param the type of the resulting value. + */ + R switchMapOn(Function>, ? extends BranchesTerminal, ? extends R>> mapping); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlNumber.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlNumber.java new file mode 100644 index 00000000000..ec3099047b8 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlNumber.java @@ -0,0 +1,182 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; +import com.mongodb.assertions.Assertions; + +import java.util.function.Function; + +/** + * A number {@linkplain MqlValue value} in the context of the MongoDB Query + * Language (MQL). {@linkplain MqlInteger Integers} are a subset of + * numbers, and so, for example, the integer 0 and the number 0 are + * {@linkplain #eq(MqlValue) equal}. + * + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlNumber extends MqlValue { + + /** + * The product of multiplying {@code this} and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlNumber multiply(MqlNumber other); + + /** + * The product of multiplying {@code this} and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + default MqlNumber multiply(final Number other) { + Assertions.notNull("other", other); + return this.multiply(MqlValues.numberToMqlNumber(other)); + } + + /** + * The quotient of dividing {@code this} value by the {@code other} value. + * This is not integer division: dividing {@code 1} by {@code 2} will + * always yield {@code 0.5}. + * + * @param other the other value. + * @return the resulting value. + */ + MqlNumber divide(MqlNumber other); + + /** + * The quotient of dividing {@code this} value by the {@code other} value. + * This is not integer division: dividing {@code 1} by {@code 2} will + * always yield {@code 0.5}. + * + * @param other the other value. + * @return the resulting value. + */ + default MqlNumber divide(final Number other) { + Assertions.notNull("other", other); + return this.divide(MqlValues.numberToMqlNumber(other)); + } + + /** + * The sum of adding {@code this} and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlNumber add(MqlNumber other); + + /** + * The sum of adding {@code this} and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + default MqlNumber add(final Number other) { + Assertions.notNull("other", other); + return this.add(MqlValues.numberToMqlNumber(other)); + } + + /** + * The difference of subtracting the {@code other} value from {@code this}. + * + * @param other the other value. + * @return the resulting value. + */ + MqlNumber subtract(MqlNumber other); + + /** + * The difference of subtracting the {@code other} value from {@code this}. + * + * @param other the other value. + * @return the resulting value. + */ + default MqlNumber subtract(final Number other) { + Assertions.notNull("other", other); + return this.subtract(MqlValues.numberToMqlNumber(other)); + } + + /** + * The {@linkplain #gt(MqlValue) larger} value of {@code this} + * and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlNumber max(MqlNumber other); + + /** + * The {@linkplain #lt(MqlValue) smaller} value of {@code this} + * and the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlNumber min(MqlNumber other); + + /** + * The integer result of rounding {@code this} to the nearest even value. + * + * @mongodb.server.release 4.2 + * @return the resulting value. + */ + MqlInteger round(); + + /** + * The result of rounding {@code this} to {@code place} decimal places + * using the "half to even" approach. + * + * @param place the decimal place to round to, from -20 to 100, exclusive. + * Positive values specify the place to the right of the + * decimal point, while negative values, to the left. + * @return the resulting value. + */ + MqlNumber round(MqlInteger place); + + /** + * The absolute value of {@code this} value. + * + * @return the resulting value. + */ + MqlNumber abs(); + + /** + * The result of passing {@code this} value to the provided function. + * Equivalent to {@code f.apply(this)}, and allows lambdas and static, + * user-defined functions to use the chaining syntax. + * + * @see MqlValue#passTo + * @param f the function to apply. + * @return the resulting value. + * @param the type of the resulting value. + */ + R passNumberTo(Function f); + + /** + * The result of applying the provided switch mapping to {@code this} value. + * + * @see MqlValue#switchOn + * @param mapping the switch mapping. + * @return the resulting value. + * @param the type of the resulting value. + */ + R switchNumberOn(Function, ? extends BranchesTerminal> mapping); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlString.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlString.java new file mode 100644 index 00000000000..dd24a8c94a2 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlString.java @@ -0,0 +1,224 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; + +import java.util.function.Function; + +import static com.mongodb.client.model.mql.MqlValues.of; + +/** + * A string {@linkplain MqlValue value} in the context of the MongoDB Query + * Language (MQL). + * + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlString extends MqlValue { + + /** + * Converts {@code this} string to lowercase. + * + * @return the resulting value. + */ + MqlString toLower(); + + /** + * Converts {@code this} string to uppercase. + * + * @return the resulting value. + */ + MqlString toUpper(); + + /** + * The result of appending the {@code other} string to the end of + * {@code this} string (strict concatenation). + * + * @param other the other value. + * @return the resulting value. + */ + MqlString append(MqlString other); + + /** + * The number of Unicode code points in {@code this} string. + * + * @return the resulting value. + */ + MqlInteger length(); + + /** + * The number of UTF-8 encoded bytes in {@code this} string. + * + * @return the resulting value. + */ + MqlInteger lengthBytes(); + + /** + * The substring of {@code this} string, from the {@code start} index + * inclusive, and including the specified {@code length}, up to + * the end of the string. + * + *

    Warning: the index position is in Unicode code points, not in + * UTF-8 encoded bytes. + * + * @param start the start index in Unicode code points. + * @param length the length in Unicode code points. + * @return the resulting value. + */ + MqlString substr(MqlInteger start, MqlInteger length); + + /** + * The substring of {@code this} string, from the {@code start} index + * inclusive, and including the specified {@code length}, up to + * the end of the string. + * + *

    Warning: the index position is in Unicode code points, not in + * UTF-8 encoded bytes. + * + * @param start the start index in Unicode code points. + * @param length the length in Unicode code points. + * @return the resulting value. + */ + default MqlString substr(final int start, final int length) { + return this.substr(of(start), of(length)); + } + + /** + * The substring of {@code this} string, from the {@code start} index + * inclusive, and including the specified {@code length}, up to + * the end of the string. + * + *

    The index position is in UTF-8 encoded bytes, not in + * Unicode code points. + * + * @param start the start index in UTF-8 encoded bytes. + * @param length the length in UTF-8 encoded bytes. + * @return the resulting value. + */ + MqlString substrBytes(MqlInteger start, MqlInteger length); + + /** + * The substring of {@code this} string, from the {@code start} index + * inclusive, and including the specified {@code length}, up to + * the end of the string. + * + *

    The index position is in UTF-8 encoded bytes, not in + * Unicode code points. + * + * @param start the start index in UTF-8 encoded bytes. + * @param length the length in UTF-8 encoded bytes. + * @return the resulting value. + */ + default MqlString substrBytes(final int start, final int length) { + return this.substrBytes(of(start), of(length)); + } + + /** + * Converts {@code this} string to an {@linkplain MqlInteger integer}. + * + *

    This will cause an error if this string does not represent an integer. + * + * @mongodb.server.release 4.0 + * @return the resulting value. + */ + MqlInteger parseInteger(); + + /** + * Converts {@code this} string to a {@linkplain MqlDate date}. + * + *

    This method behaves like {@link #parseDate(MqlString)}, + * with the default format, which is {@code "%Y-%m-%dT%H:%M:%S.%LZ"}. + * + *

    Will cause an error if this string does not represent a valid + * date string (such as "2018-03-20", "2018-03-20T12:00:00Z", or + * "2018-03-20T12:00:00+0500"). + * + * @see MqlDate#asString() + * @see MqlDate#asString(MqlString, MqlString) + * @return the resulting value. + */ + MqlDate parseDate(); + + /** + * Converts {@code this} string to a {@linkplain MqlDate date}, + * using the specified {@code format}. UTC is assumed if the timezone + * offset element is not specified in the format. + * + *

    Will cause an error if {@code this} string does not match the + * specified {@code format}. + * Will cause an error if an element is specified that is finer-grained + * than an element that is not specified, with year being coarsest + * (for example, minute is specified, but hour is not). + * Omitted finer-grained elements will be parsed to 0. + * + * @see MqlDate#asString() + * @see MqlDate#asString(MqlString, MqlString) + * @mongodb.server.release 4.0 + * @mongodb.driver.manual reference/operator/aggregation/dateFromString/ Format Specifiers, UTC Offset, and Olson Timezone Identifier + * @param format the format. + * @return the resulting value. + */ + MqlDate parseDate(MqlString format); + + /** + * Converts {@code this} string to a {@linkplain MqlDate date}, + * using the specified {@code timezone} and {@code format}. + * + + *

    Will cause an error if {@code this} string does not match the + * specified {@code format}. + * Will cause an error if an element is specified that is finer-grained + * than an element that is not specified, with year being coarsest + * (for example, minute is specified, but hour is not). + * Omitted finer-grained elements will be parsed to 0. + * Will cause an error if the format includes an offset or + * timezone, even if it matches the supplied {@code timezone}. + * + * @see MqlDate#asString() + * @see MqlDate#asString(MqlString, MqlString) + * @mongodb.driver.manual reference/operator/aggregation/dateFromString/ Format Specifiers, UTC Offset, and Olson Timezone Identifier + * @param format the format. + * @param timezone the UTC Offset or Olson Timezone Identifier. + * @return the resulting value. + */ + MqlDate parseDate(MqlString timezone, MqlString format); + + /** + * The result of passing {@code this} value to the provided function. + * Equivalent to {@code f.apply(this)}, and allows lambdas and static, + * user-defined functions to use the chaining syntax. + * + * @see MqlValue#passTo + * @param f the function to apply. + * @return the resulting value. + * @param the type of the resulting value. + */ + R passStringTo(Function f); + + /** + * The result of applying the provided switch mapping to {@code this} value. + * + * @see MqlValue#switchOn + * @param mapping the switch mapping. + * @return the resulting value. + * @param the type of the resulting value. + */ + R switchStringOn(Function, ? extends BranchesTerminal> mapping); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlUnchecked.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlUnchecked.java new file mode 100644 index 00000000000..ec53a927b4e --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlUnchecked.java @@ -0,0 +1,80 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Sealed; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Documents places where the API relies on a user asserting + * something that is not checked at run-time. + * If the assertion turns out to be false, the API behavior is unspecified. + * + *

    This class is not part of the public API and may be removed or changed at any time

    + */ +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.TYPE_USE}) +@Sealed +public @interface MqlUnchecked { + /** + * @return A hint on the user assertion the API relies on. + */ + Unchecked[] value(); + + /** + * @see MqlUnchecked#value() + */ + enum Unchecked { + /** + * The API relies on the values it encounters being of the + * (raw or non-parameterized) type + * implied, specified by, or inferred from the user code. + * + *

    For example, {@link MqlDocument#getBoolean(String)} + * relies on the values of the document field being of the + * {@linkplain MqlBoolean boolean} type. + */ + TYPE, + /** + * The API checks the raw type, but relies on the type argument + * implied, specified by, or inferred from user code. + * + *

    For example, {@link MqlValue#isArrayOr(MqlArray)} + * checks that the value is of the + * {@linkplain MqlArray array} raw type, + * but relies on the elements of the array being of + * the type derived from the user code. + */ + TYPE_ARGUMENT, + /** + * The presence of the specified value is not checked by the API. + * The use of the annotated method is an unchecked assertion that the + * specified (whether by index, name, key, position, or otherwise) + * element is present in the structure involved. + * + *

    For example, {@link MqlDocument#getField(String)} relies + * on the field being present, and {@link MqlArray#first} relies + * on the array being non-empty. + */ + PRESENT, + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlValue.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlValue.java new file mode 100644 index 00000000000..9366ce77fe9 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlValue.java @@ -0,0 +1,334 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Sealed; + +import java.util.function.Function; + +import static com.mongodb.client.model.mql.MqlUnchecked.Unchecked.TYPE_ARGUMENT; + +/** + * A value in the context of the MongoDB Query Language (MQL). + * + *

    The API provided by this base type and its subtypes is the Java-native + * variant of MQL. It is used to query the MongoDB server, to perform remote + * computations, to store and retrieve data, or to otherwise work with data on + * a MongoDB server or compatible execution context. Though the methods exposed + * through this API generally correspond to MQL operations, this correspondence + * is not exact. + * + *

    The following is an example of usage within an aggregation pipeline. Here, + * the current document value is obtained and its "numberArray" field is + * filtered and summed, in a style similar to that of the Java Stream API: + * + *

    {@code
    + * import static com.mongodb.client.model.mql.MqlValues.current;
    + * MongoCollection col = ...;
    + * AggregateIterable result = col.aggregate(Arrays.asList(
    + *     addFields(new Field<>("result", current()
    + *         .getArray("numberArray")
    + *         .filter(v -> v.gt(of(0)))
    + *         .sum(v -> v)))));
    + * }
    + * + *

    Values are typically initially obtained via the current document and its + * fields, or specified via statically-imported methods on the + * {@link MqlValues} class. + * + *

    As with the Java Stream API's terminal operations, corresponding Java + * values are not directly available, but must be obtained indirectly via + * {@code MongoCollection.aggregate} or {@code MongoCollection.find}. + * Certain methods may cause an error, which will be produced + * through these "terminal operations". + * + *

    The null value is not part of, and cannot be used as if it were part + * of, any explicit type (except the root type {@link MqlValue} itself). + * See {@link MqlValues#ofNull} for more details. + * + *

    This API specifies no "missing" or "undefined" value. Users may use + * {@link MqlMap#has} to check whether a value is present. + * + *

    This type hierarchy differs from the {@linkplain org.bson} types in that + * they provide computational operations, the numeric types are less granular, + * and it offers multiple abstractions of certain types (document, map, entry). + * It differs from the corresponding Java types (such as {@code int}, + * {@link String}, {@link java.util.Map}) in that the operations + * available differ, and in that an implementation of this API may be used to + * produce MQL in the form of BSON. (This API makes no guarantee regarding the + * BSON output produced by its implementation, which in any case may vary due + * to optimization or other factors.) + * + *

    Some methods within the API constitute an assertion by the user that the + * data is of a certain type. For example, {@link MqlDocument#getArray}} + * requires that the underlying field is both an array, and an array of some + * certain type. If the field is not an array in the underlying data, behaviour + * is undefined by this API (though behaviours may be defined by the execution + * context, users are strongly discouraged from relying on behaviour that is not + * part of this API). + * + *

    This API should be treated as sealed: + * it must not be extended or implemented (unless explicitly allowed). + * + * @see MqlValues + * @since 4.9.0 + */ +@Sealed +@Beta(Beta.Reason.CLIENT) +public interface MqlValue { + + /** + * The method {@link MqlValue#eq} should be used to compare values for + * equality. This method checks reference equality. + */ + @Override + boolean equals(Object other); + + /** + * Whether {@code this} value is equal to the {@code other} value. + * + *

    The result does not correlate with {@link MqlValue#equals(Object)}. + * + * @param other the other value. + * @return the resulting value. + */ + MqlBoolean eq(MqlValue other); + + /** + * Whether {@code this} value is not equal to the {@code other} value. + * + *

    The result does not correlate with {@link MqlValue#equals(Object)}. + * + * @param other the other value. + * @return the resulting value. + */ + MqlBoolean ne(MqlValue other); + + /** + * Whether {@code this} value is greater than the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlBoolean gt(MqlValue other); + + /** + * Whether {@code this} value is greater than or equal to the {@code other} + * value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlBoolean gte(MqlValue other); + + /** + * Whether {@code this} value is less than the {@code other} value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlBoolean lt(MqlValue other); + + /** + * Whether {@code this} value is less than or equal to the {@code other} + * value. + * + * @param other the other value. + * @return the resulting value. + */ + MqlBoolean lte(MqlValue other); + + /** + * {@code this} value as a {@linkplain MqlBoolean boolean} if + * {@code this} is a boolean, or the {@code other} boolean value if + * {@code this} is null, or is missing, or is of any other non-boolean type. + * + * @param other the other value. + * @return the resulting value. + */ + MqlBoolean isBooleanOr(MqlBoolean other); + + /** + * {@code this} value as a {@linkplain MqlNumber number} if + * {@code this} is a number, or the {@code other} number value if + * {@code this} is null, or is missing, or is of any other non-number type. + * + * @mongodb.server.release 4.4 + * @param other the other value. + * @return the resulting value. + */ + MqlNumber isNumberOr(MqlNumber other); + + /** + * {@code this} value as an {@linkplain MqlInteger integer} if + * {@code this} is an integer, or the {@code other} integer value if + * {@code this} is null, or is missing, or is of any other non-integer type. + * + * @mongodb.server.release 5.2 + * @param other the other value. + * @return the resulting value. + */ + MqlInteger isIntegerOr(MqlInteger other); + + /** + * {@code this} value as a {@linkplain MqlString string} if + * {@code this} is a string, or the {@code other} string value if + * {@code this} is null, or is missing, or is of any other non-string type. + * + * @param other the other value. + * @return the resulting value. + */ + MqlString isStringOr(MqlString other); + + /** + * {@code this} value as a {@linkplain MqlDate boolean} if + * {@code this} is a date, or the {@code other} date value if + * {@code this} is null, or is missing, or is of any other non-date type. + * + * @param other the other value. + * @return the resulting value. + */ + MqlDate isDateOr(MqlDate other); + + /** + * {@code this} value as a {@linkplain MqlArray array} if + * {@code this} is an array, or the {@code other} array value if + * {@code this} is null, or is missing, or is of any other non-array type. + * + *

    Warning: The type of the elements of the resulting array are not + * enforced by the API. The specification of a type by the user is an + * unchecked assertion that all elements are of that type. + * If the array contains multiple types (such as both nulls and integers) + * then a super-type encompassing all types must be chosen, and + * if necessary the elements should be individually type-checked when used. + * + * @param other the other value. + * @return the resulting value. + * @param the type of the elements of the resulting array. + */ + MqlArray<@MqlUnchecked(TYPE_ARGUMENT) T> isArrayOr(MqlArray other); + + /** + * {@code this} value as a {@linkplain MqlDocument document} if + * {@code this} is a document (or document-like value, see + * {@link MqlMap} and {@link MqlEntry}) + * or the {@code other} document value if {@code this} is null, + * or is missing, or is of any other non-document type. + * + * @param other the other value. + * @return the resulting value. + * @param the type. + */ + T isDocumentOr(T other); + + /** + * {@code this} value as a {@linkplain MqlMap map} if + * {@code this} is a map (or map-like value, see + * {@link MqlDocument} and {@link MqlEntry}) + * or the {@code other} map value if {@code this} is null, + * or is missing, or is of any other non-map type. + * + *

    Warning: The type of the values of the resulting map are not + * enforced by the API. The specification of a type by the user is an + * unchecked assertion that all map values are of that type. + * If the map contains multiple types (such as both nulls and integers) + * then a super-type encompassing all types must be chosen, and + * if necessary the elements should be individually type-checked when used. + * + * @param other the other value. + * @return the resulting value. + * @param the type of the values of the resulting map. + */ + MqlMap<@MqlUnchecked(TYPE_ARGUMENT) T> isMapOr(MqlMap other); + + /** + * The {@linkplain MqlString string} representation of {@code this} value. + * + *

    This will cause an error if the type cannot be converted + * to a {@linkplain MqlString string}, as is the case with + * {@linkplain MqlArray arrays}, + * {@linkplain MqlDocument documents}, + * {@linkplain MqlMap maps}, + * {@linkplain MqlEntry entries}, and the + * {@linkplain MqlValues#ofNull() null value}. + * + * @mongodb.server.release 4.0 + * @see MqlString#parseDate() + * @see MqlString#parseInteger() + * @return the resulting value. + */ + MqlString asString(); + + /** + * The result of passing {@code this} value to the provided function. + * Equivalent to {@code f.apply(this)}, and allows lambdas and static, + * user-defined functions to use the chaining syntax. + * + *

    The appropriate type-based variant should be used when the type + * of {@code this} is known. + * + * @see MqlBoolean#passBooleanTo + * @see MqlInteger#passIntegerTo + * @see MqlNumber#passNumberTo + * @see MqlString#passStringTo + * @see MqlDate#passDateTo + * @see MqlArray#passArrayTo + * @see MqlMap#passMapTo + * @see MqlDocument#passDocumentTo + * + * @param f the function to apply. + * @return the resulting value. + * @param the type of the resulting value. + */ + R passTo(Function f); + + /** + * The result of applying the provided switch mapping to {@code this} value. + * + *

    Can be used to perform pattern matching on the type of {@code this} + * value, or to perform comparisons, or to perform any arbitrary check on + * {@code this} value. + * + *

    The suggested convention is to use "{@code on}" as the name of the + * {@code mapping} parameter, for example: + * + *

    {@code
    +     * myValue.switchOn(on -> on
    +     *     .isInteger(...)
    +     *     ...
    +     *     .defaults(...))
    +     * }
    + * + *

    The appropriate type-based variant should be used when the type + * of {@code this} is known. + * + * @see MqlBoolean#switchBooleanOn + * @see MqlInteger#switchIntegerOn + * @see MqlNumber#switchNumberOn + * @see MqlString#switchStringOn + * @see MqlDate#switchDateOn + * @see MqlArray#switchArrayOn + * @see MqlMap#switchMapOn + * @see MqlDocument#switchDocumentOn + * + * @param mapping the switch mapping. + * @return the resulting value. + * @param the type of the resulting value. + */ + R switchOn(Function, ? extends BranchesTerminal> mapping); +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlValues.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlValues.java new file mode 100644 index 00000000000..c91994872b3 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlValues.java @@ -0,0 +1,412 @@ +/* + * 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.mql; + +import com.mongodb.annotations.Beta; +import com.mongodb.assertions.Assertions; +import org.bson.BsonArray; +import org.bson.BsonBoolean; +import org.bson.BsonDateTime; +import org.bson.BsonDecimal128; +import org.bson.BsonDocument; +import org.bson.BsonDouble; +import org.bson.BsonInt32; +import org.bson.BsonInt64; +import org.bson.BsonNull; +import org.bson.BsonString; +import org.bson.BsonValue; +import org.bson.conversions.Bson; +import org.bson.types.Decimal128; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static com.mongodb.client.model.mql.MqlExpression.AstPlaceholder; +import static com.mongodb.client.model.mql.MqlExpression.toBsonValue; +import static com.mongodb.client.model.mql.MqlUnchecked.Unchecked.TYPE_ARGUMENT; + +/** + * Convenience methods related to {@link MqlValue}, used primarily to + * produce values in the context of the MongoDB Query Language (MQL). + * + * @since 4.9.0 + */ +@Beta(Beta.Reason.CLIENT) +public final class MqlValues { + + private MqlValues() {} + + /** + * Returns a {@linkplain MqlBoolean boolean} value corresponding to + * the provided {@code boolean} primitive. + * + * @param of the {@code boolean} primitive. + * @return the resulting value. + */ + public static MqlBoolean of(final boolean of) { + // we intentionally disallow ofBoolean(null) + return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonBoolean(of))); + } + + /** + * Returns an {@linkplain MqlArray array} of + * {@linkplain MqlBoolean booleans} corresponding to + * the provided {@code boolean} primitives. + * + * @param array the array. + * @return the resulting value. + */ + public static MqlArray ofBooleanArray(final boolean... array) { + Assertions.notNull("array", array); + List list = new ArrayList<>(); + for (boolean b : array) { + list.add(new BsonBoolean(b)); + } + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list))); + } + + /** + * Returns an {@linkplain MqlInteger integer} value corresponding to + * the provided {@code int} primitive. + * + * @param of the {@code int} primitive. + * @return the resulting value. + */ + public static MqlInteger of(final int of) { + return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonInt32(of))); + } + + /** + * Returns an {@linkplain MqlArray array} of + * {@linkplain MqlInteger integers} corresponding to + * the provided {@code int} primitives. + * + * @param array the array. + * @return the resulting value. + */ + public static MqlArray ofIntegerArray(final int... array) { + Assertions.notNull("array", array); + List list = new ArrayList<>(); + for (int i : array) { + list.add(new BsonInt32(i)); + } + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list))); + } + + /** + * Returns an {@linkplain MqlInteger integer} value corresponding to + * the provided {@code long} primitive. + * + * @param of the {@code long} primitive. + * @return the resulting value. + */ + public static MqlInteger of(final long of) { + return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonInt64(of))); + } + + /** + * Returns an {@linkplain MqlArray array} of + * {@linkplain MqlInteger integers} corresponding to + * the provided {@code long} primitives. + * + * @param array the array. + * @return the resulting value. + */ + public static MqlArray ofIntegerArray(final long... array) { + Assertions.notNull("array", array); + List list = new ArrayList<>(); + for (long i : array) { + list.add(new BsonInt64(i)); + } + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list))); + } + + /** + * Returns a {@linkplain MqlNumber number} value corresponding to + * the provided {@code double} primitive. + * + * @param of the {@code double} primitive. + * @return the resulting value. + */ + public static MqlNumber of(final double of) { + return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonDouble(of))); + } + + /** + * Returns an {@linkplain MqlArray array} of + * {@linkplain MqlNumber numbers} corresponding to + * the provided {@code double} primitives. + * + * @param array the array. + * @return the resulting value. + */ + public static MqlArray ofNumberArray(final double... array) { + Assertions.notNull("array", array); + List list = new ArrayList<>(); + for (double n : array) { + list.add(new BsonDouble(n)); + } + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list))); + } + + /** + * Returns a {@linkplain MqlNumber number} value corresponding to + * the provided {@link Decimal128}. + * + * @param of the {@link Decimal128}. + * @return the resulting value. + */ + public static MqlNumber of(final Decimal128 of) { + Assertions.notNull("Decimal128", of); + return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonDecimal128(of))); + } + + /** + * Returns an {@linkplain MqlArray array} of + * {@linkplain MqlNumber numbers} corresponding to + * the provided {@link Decimal128}s. + * + * @param array the array. + * @return the resulting value. + */ + public static MqlArray ofNumberArray(final Decimal128... array) { + Assertions.notNull("array", array); + List result = new ArrayList<>(); + for (Decimal128 e : array) { + Assertions.notNull("elements of array", e); + result.add(new BsonDecimal128(e)); + } + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(result))); + } + + /** + * Returns a {@linkplain MqlDate date and time} value corresponding to + * the provided {@link Instant}. + * + * @param of the {@link Instant}. + * @return the resulting value. + */ + public static MqlDate of(final Instant of) { + Assertions.notNull("Instant", of); + return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonDateTime(of.toEpochMilli()))); + } + + /** + * Returns an {@linkplain MqlArray array} of + * {@linkplain MqlDate dates} corresponding to + * the provided {@link Instant}s. + * + * @param array the array. + * @return the resulting value. + */ + public static MqlArray ofDateArray(final Instant... array) { + Assertions.notNull("array", array); + List result = new ArrayList<>(); + for (Instant e : array) { + Assertions.notNull("elements of array", e); + result.add(new BsonDateTime(e.toEpochMilli())); + } + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(result))); + } + + /** + * Returns an {@linkplain MqlString string} value corresponding to + * the provided {@link String}. + * + * @param of the {@link String}. + * @return the resulting value. + */ + public static MqlString of(final String of) { + Assertions.notNull("String", of); + return new MqlExpression<>((codecRegistry) -> new AstPlaceholder(new BsonString(of))); + } + + /** + * Returns an {@linkplain MqlArray array} of + * {@linkplain MqlString strings} corresponding to + * the provided {@link String}s. + * + * @param array the array. + * @return the resulting value. + */ + public static MqlArray ofStringArray(final String... array) { + Assertions.notNull("array", array); + List result = new ArrayList<>(); + for (String e : array) { + Assertions.notNull("elements of array", e); + result.add(new BsonString(e)); + } + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(result))); + } + + /** + * Returns a reference to the "current" + * {@linkplain MqlDocument document} value. + * The "current" value is the top-level document currently being processed + * in the aggregation pipeline stage. + * + * @return a reference to the current value + */ + public static MqlDocument current() { + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonString("$$CURRENT"))) + .assertImplementsAllExpressions(); + } + + /** + * Returns a reference to the "current" + * value as a {@linkplain MqlMap map} value. + * The "current" value is the top-level document currently being processed + * in the aggregation pipeline stage. + * + *

    Warning: The type of the values of the resulting map are not + * enforced by the API. The specification of a type by the user is an + * unchecked assertion that all map values are of that type. + * If the map contains multiple types (such as both nulls and integers) + * then a super-type encompassing all types must be chosen, and + * if necessary the elements should be individually type-checked when used. + * + * @return a reference to the current value as a map. + * @param the type of the map's values. + */ + public static MqlMap<@MqlUnchecked(TYPE_ARGUMENT) R> currentAsMap() { + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonString("$$CURRENT"))) + .assertImplementsAllExpressions(); + } + + /** + * Returns an {@linkplain MqlDocument array} value, containing the + * {@linkplain MqlValue values} provided. + * + * @param array the {@linkplain MqlValue values}. + * @return the resulting value. + * @param the type of the array elements. + */ + @SafeVarargs // nothing is stored in the array + public static MqlArray ofArray(final T... array) { + Assertions.notNull("array", array); + return new MqlExpression<>((cr) -> { + List list = new ArrayList<>(); + for (T v : array) { + Assertions.notNull("elements of array", v); + list.add(((MqlExpression) v).toBsonValue(cr)); + } + return new AstPlaceholder(new BsonArray(list)); + }); + } + + /** + * Returns an {@linkplain MqlEntry entry} value. + * + * @param k the key. + * @param v the value. + * @return the resulting value. + * @param the type of the key. + */ + public static MqlEntry ofEntry(final MqlString k, final T v) { + Assertions.notNull("k", k); + Assertions.notNull("v", v); + return new MqlExpression<>((cr) -> { + BsonDocument document = new BsonDocument(); + document.put("k", toBsonValue(cr, k)); + document.put("v", toBsonValue(cr, v)); + return new AstPlaceholder(document); + }); + } + + /** + * Returns an empty {@linkplain MqlMap map} value. + * + * @param the type of the resulting map's values. + * @return the resulting map value. + */ + public static MqlMap ofMap() { + return ofMap(new BsonDocument()); + } + + /** + * Returns a {@linkplain MqlMap map} value corresponding to the + * provided {@link Bson Bson document}. + * + *

    Warning: The type of the values of the resulting map are not + * enforced by the API. The specification of a type by the user is an + * unchecked assertion that all map values are of that type. + * If the map contains multiple types (such as both nulls and integers) + * then a super-type encompassing all types must be chosen, and + * if necessary the elements should be individually type-checked when used. + * + * @param map the map as a {@link Bson Bson document}. + * @param the type of the resulting map's values. + * @return the resulting map value. + */ + public static MqlMap<@MqlUnchecked(TYPE_ARGUMENT) T> ofMap(final Bson map) { + Assertions.notNull("map", map); + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonDocument("$literal", + map.toBsonDocument(BsonDocument.class, cr)))); + } + + /** + * Returns a {@linkplain MqlDocument document} value corresponding to the + * provided {@link Bson Bson document}. + * + * @param document the {@linkplain Bson BSON document}. + * @return the resulting value. + */ + public static MqlDocument of(final Bson document) { + Assertions.notNull("document", document); + // All documents are wrapped in a $literal; this is the least brittle approach. + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonDocument("$literal", + document.toBsonDocument(BsonDocument.class, cr)))); + } + + /** + * The null value in the context of the MongoDB Query Language (MQL). + * + *

    The null value is not part of, and cannot be used as if it were part + * of, any explicit type (except the root type {@link MqlValue} itself). + * It has no explicit type of its own. + * + *

    Instead of checking that a value is null, users should generally + * check that a value is of their expected type, via methods such as + * {@link MqlValue#isNumberOr(MqlNumber)}. Where the null value + * must be checked explicitly, users may use {@link Branches#isNull} within + * {@link MqlValue#switchOn}. + * + * @return the null value + */ + public static MqlValue ofNull() { + // There is no specific mql type corresponding to Null, + // and Null is not a value in any other mql type. + return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonNull())) + .assertImplementsAllExpressions(); + } + + static MqlNumber numberToMqlNumber(final Number number) { + Assertions.notNull("number", number); + if (number instanceof Integer) { + return of((int) number); + } else if (number instanceof Long) { + return of((long) number); + } else if (number instanceof Double) { + return of((double) number); + } else if (number instanceof Decimal128) { + return of((Decimal128) number); + } else { + throw new IllegalArgumentException("Number must be one of: Integer, Long, Double, Decimal128"); + } + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/SwitchCase.java b/driver-core/src/main/com/mongodb/client/model/mql/SwitchCase.java new file mode 100644 index 00000000000..3210a9bfdf2 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/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.mql; + +final class SwitchCase { + private final MqlBoolean caseValue; + private final R thenValue; + + SwitchCase(final MqlBoolean caseValue, final R thenValue) { + this.caseValue = caseValue; + this.thenValue = thenValue; + } + + MqlBoolean getCaseValue() { + return caseValue; + } + + R getThenValue() { + return thenValue; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/mql/package-info.java b/driver-core/src/main/com/mongodb/client/model/mql/package-info.java new file mode 100644 index 00000000000..08cbc6195a7 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/mql/package-info.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + +/** + * @see com.mongodb.client.model.mql.MqlValue + * @see com.mongodb.client.model.mql.MqlValues + * @since 4.9.0 + */ +@Beta(Beta.Reason.CLIENT) +@NonNullApi +package com.mongodb.client.model.mql; +import com.mongodb.annotations.Beta; +import com.mongodb.lang.NonNullApi; diff --git a/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java b/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java index 0db3482dfe5..02ea44a25a5 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/AggregatesTest.java @@ -22,35 +22,22 @@ import org.bson.Document; import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.bson.conversions.Bson; import org.junit.jupiter.api.Test; import static com.mongodb.ClusterFixture.serverVersionAtLeast; -import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry; -import static com.mongodb.client.model.GeoNearOptions.geoNearOptions; import static com.mongodb.client.model.Aggregates.geoNear; import static com.mongodb.client.model.Aggregates.unset; +import static com.mongodb.client.model.GeoNearOptions.geoNearOptions; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assumptions.assumeTrue; public class AggregatesTest extends OperationTest { - private List assertPipeline(final String stageAsString, final Bson stage) { - BsonDocument expectedStage = BsonDocument.parse(stageAsString); - List pipeline = Collections.singletonList(stage); - assertEquals(expectedStage, pipeline.get(0).toBsonDocument(BsonDocument.class, getDefaultCodecRegistry())); - return pipeline; - } - private void assertResults(final List pipeline, final String s) { - List expectedResults = parseToList(s); - List results = getCollectionHelper().aggregate(pipeline); - assertEquals(expectedResults, results); - } @Test public void testUnset() { diff --git a/driver-core/src/test/functional/com/mongodb/client/model/OperationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/OperationTest.java index ac0558f3c34..8fb89a90ece 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/OperationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/OperationTest.java @@ -22,12 +22,13 @@ import com.mongodb.internal.connection.ServerHelper; import org.bson.BsonArray; import org.bson.BsonDocument; -import org.bson.Document; +import org.bson.codecs.BsonDocumentCodec; import org.bson.codecs.DecoderContext; -import org.bson.codecs.DocumentCodec; +import org.bson.conversions.Bson; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -36,6 +37,7 @@ import static com.mongodb.ClusterFixture.getBinding; import static com.mongodb.ClusterFixture.getPrimary; import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry; +import static org.junit.jupiter.api.Assertions.assertEquals; public abstract class OperationTest { @@ -53,12 +55,12 @@ public void afterEach() { ServerHelper.checkPool(getPrimary()); } - CollectionHelper getCollectionHelper() { + protected CollectionHelper getCollectionHelper() { return getCollectionHelper(getNamespace()); } - private CollectionHelper getCollectionHelper(final MongoNamespace namespace) { - return new CollectionHelper<>(new DocumentCodec(), namespace); + private CollectionHelper getCollectionHelper(final MongoNamespace namespace) { + return new CollectionHelper<>(new BsonDocumentCodec(), namespace); } private String getDatabaseName() { @@ -73,11 +75,29 @@ MongoNamespace getNamespace() { return new MongoNamespace(getDatabaseName(), getCollectionName()); } - public static List parseToList(final String s) { - return BsonArray.parse(s).stream().map(v -> toDocument(v.asDocument())).collect(Collectors.toList()); + static List parseToList(final String s) { + return BsonArray.parse(s).stream().map(v -> toBsonDocument(v.asDocument())).collect(Collectors.toList()); } - public static Document toDocument(final BsonDocument bsonDocument) { - return getDefaultCodecRegistry().get(Document.class).decode(bsonDocument.asBsonReader(), DecoderContext.builder().build()); + public static BsonDocument toBsonDocument(final BsonDocument bsonDocument) { + return getDefaultCodecRegistry().get(BsonDocument.class).decode(bsonDocument.asBsonReader(), DecoderContext.builder().build()); + } + + + protected List assertPipeline(final String stageAsString, final Bson stage) { + List pipeline = Collections.singletonList(stage); + return assertPipeline(stageAsString, pipeline); + } + + protected List assertPipeline(final String stageAsString, final List pipeline) { + BsonDocument expectedStage = BsonDocument.parse(stageAsString); + assertEquals(expectedStage, pipeline.get(0).toBsonDocument(BsonDocument.class, getDefaultCodecRegistry())); + return pipeline; + } + + protected void assertResults(final List pipeline, final String expectedResultsAsString) { + List expectedResults = parseToList(expectedResultsAsString); + List results = getCollectionHelper().aggregate(pipeline); + assertEquals(expectedResults, results); } } diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/AbstractMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/AbstractMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..31a5cecd91f --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/AbstractMqlValuesFunctionalTest.java @@ -0,0 +1,130 @@ +/* + * 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.mql; + +import com.mongodb.client.model.Field; +import com.mongodb.client.model.OperationTest; +import com.mongodb.lang.Nullable; +import org.bson.BsonArray; +import org.bson.BsonDocument; +import org.bson.BsonReader; +import org.bson.BsonString; +import org.bson.BsonValue; +import org.bson.Document; +import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.BsonValueCodecProvider; +import org.bson.codecs.DecoderContext; +import org.bson.conversions.Bson; +import org.bson.json.JsonReader; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +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 AbstractMqlValuesFunctionalTest extends OperationTest { + + /** + * Java stand-in for the "missing" value. + */ + public static final Object MISSING = new Object(); + + @BeforeEach + public void setUp() { + getCollectionHelper().drop(); + } + + @AfterEach + public void tearDown() { + getCollectionHelper().drop(); + } + + protected void assertExpression(@Nullable final Object expected, final MqlValue mqlValue) { + assertExpression(expected, mqlValue, null); + } + + protected void assertExpression(@Nullable final Object expected, final MqlValue mqlValue, @Nullable final String expectedMql) { + assertEval(expected, mqlValue); + + if (expectedMql == null) { + return; + } + + BsonValue expressionValue = ((MqlExpression) mqlValue).toBsonValue( + fromProviders(new BsonValueCodecProvider(), DEFAULT_CODEC_REGISTRY)); + BsonValue bsonValue = new BsonDocumentFragmentCodec().readValue( + new JsonReader(expectedMql), + DecoderContext.builder().build()); + assertEquals(bsonValue, expressionValue, expressionValue.toString().replace("\"", "'")); + } + + private void assertEval(@Nullable final Object expected, final MqlValue toEvaluate) { + BsonValue evaluated = evaluate(toEvaluate); + if (expected == MISSING && evaluated == null) { + // if the "val" field was removed by "missing", then evaluated is null + return; + } + BsonValue expected1 = toBsonValue(expected); + assertEquals(expected1, evaluated); + } + + protected BsonValue toBsonValue(@Nullable final Object value) { + if (value instanceof BsonValue) { + return (BsonValue) value; + } + return new Document("val", value).toBsonDocument().get("val"); + } + + @Nullable + protected BsonValue evaluate(final MqlValue toEvaluate) { + Bson addFieldsStage = addFields(new Field<>("val", toEvaluate)); + List stages = new ArrayList<>(); + stages.add(addFieldsStage); + List results; + if (getCollectionHelper().count() == 0) { + BsonDocument document = new BsonDocument("val", new BsonString("#invalid string#")); + if (serverVersionAtLeast(5, 1)) { + Bson documentsStage = new BsonDocument("$documents", new BsonArray(Arrays.asList(document))); + stages.add(0, documentsStage); + results = getCollectionHelper().aggregateDb(stages); + } else { + getCollectionHelper().insertDocuments(document); + results = getCollectionHelper().aggregate(stages); + getCollectionHelper().drop(); + } + } else { + results = getCollectionHelper().aggregate(stages); + } + return results.get(0).get("val"); + } + + private static class BsonDocumentFragmentCodec extends BsonDocumentCodec { + public BsonValue readValue(final BsonReader reader, final DecoderContext decoderContext) { + reader.readBsonType(); + return super.readValue(reader, decoderContext); + } + } + +} + diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/ArithmeticMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/ArithmeticMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..cbc9451ffe3 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/ArithmeticMqlValuesFunctionalTest.java @@ -0,0 +1,284 @@ +/* + * 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.mql; + +import org.bson.types.Decimal128; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.model.mql.MqlValues.numberToMqlNumber; +import static com.mongodb.client.model.mql.MqlValues.of; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +@SuppressWarnings("ConstantConditions") +class ArithmeticMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#arithmetic-expression-operators + + @Test + public void literalsTest() { + assertExpression(1, of(1), "1"); + assertExpression(1L, of(1L)); + assertExpression(1.0, of(1.0)); + assertExpression(Decimal128.parse("1.0"), of(Decimal128.parse("1.0"))); + assertThrows(IllegalArgumentException.class, () -> of((Decimal128) null)); + + // expression equality differs from bson equality + assertExpression(true, of(1L).eq(of(1.0))); + assertExpression(true, of(1L).eq(of(1))); + + // bson equality; underlying type is preserved + // this behaviour is not defined by the API, but tested for clarity + assertEquals(toBsonValue(1), evaluate(of(1))); + assertEquals(toBsonValue(1L), evaluate(of(1L))); + assertEquals(toBsonValue(1.0), evaluate(of(1.0))); + assertNotEquals(toBsonValue(1), evaluate(of(1L))); + assertNotEquals(toBsonValue(1.0), evaluate(of(1L))); + + // Number conversions; used internally + assertExpression(1, numberToMqlNumber(1)); + assertExpression(1L, numberToMqlNumber(1L)); + assertExpression(1.0, numberToMqlNumber(1.0)); + assertExpression(Decimal128.parse("1.0"), numberToMqlNumber(Decimal128.parse("1.0"))); + assertThrows(IllegalArgumentException.class, + () -> assertExpression("n/a", numberToMqlNumber(BigDecimal.valueOf(1)))); + } + + @Test + public void multiplyTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/multiply/ + assertExpression( + 2.0 * 2, + of(2.0).multiply(of(2)), + "{'$multiply': [2.0, 2]}"); + + // mixing integers and numbers + MqlInteger oneInt = of(1); + MqlNumber oneNum = of(1.0); + MqlInteger resultInt = oneInt.multiply(oneInt); + MqlNumber resultNum = oneNum.multiply(oneNum); + // compile time error if these were IntegerExpressions: + MqlNumber r2 = oneNum.multiply(oneInt); + MqlNumber r3 = oneInt.multiply(oneNum); + assertExpression(1, resultInt); + // 1 is also a valid expected value in our API + assertExpression(1.0, resultNum); + assertExpression(1.0, r2); + assertExpression(1.0, r3); + + // convenience + assertExpression(2.0, of(1.0).multiply(2.0)); + assertExpression(2L, of(1).multiply(2L)); + assertExpression(2, of(1).multiply(2)); + } + + @SuppressWarnings("PointlessArithmeticExpression") + @Test + public void divideTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/divide/ + assertExpression( + 2.0 / 1.0, + of(2.0).divide(of(1.0)), + "{'$divide': [2.0, 1.0]}"); + + // division always converts to a double: + assertExpression( + 2.0, // not: 2 / 1 + of(2).divide(of(1)), + "{'$divide': [2, 1]}"); + + // this means that unlike Java's 1/2==0, dividing any underlying + // BSON number type always yields an equal result: + assertExpression( + 1.0 / 2.0, + of(1.0).divide(of(2.0)), + "{'$divide': [1.0, 2.0]}"); + assertExpression( + 0.5, + of(1).divide(of(2)), + "{'$divide': [1, 2]}"); + + // however, there are differences in evaluation between numbers + // represented using Decimal128 and double: + assertExpression( + 2.5242187499999997, + of(3.231).divide(of(1.28))); + assertExpression( + Decimal128.parse("2.52421875"), + of(Decimal128.parse("3.231")).divide(of(Decimal128.parse("1.28")))); + assertExpression( + Decimal128.parse("2.52421875"), + of(Decimal128.parse("3.231")).divide(of(1.28))); + assertExpression( + Decimal128.parse("2.524218750000"), + of(3.231).divide(of(Decimal128.parse("1.28")))); + // this is not simply because the Java literal used has no corresponding + // double value - it is the same value as-written: + assertEquals("3.231", "" + 3.231); + assertEquals("1.28", "" + 1.28); + + + // convenience + assertExpression(0.5, of(1.0).divide(2.0)); + assertExpression(0.5, of(1).divide(2.0)); + assertExpression(0.5, of(1).divide(2L)); + assertExpression(0.5, of(1).divide(2)); + + // divide always returns a Number, so the method is not on IntegerExpression + } + + @Test + public void addTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/add/ + MqlInteger actual = of(2).add(of(2)); + assertExpression( + 2 + 2, actual, + "{'$add': [2, 2]}"); + assertExpression( + 2.0 + 2, + of(2.0).add(of(2)), + "{'$add': [2.0, 2]}"); + + // overflows into a supported underlying type + assertExpression( + Integer.MAX_VALUE + 2L, + of(Integer.MAX_VALUE).add(of(2))); + assertExpression( + Long.MAX_VALUE + 2.0, + of(Long.MAX_VALUE).add(of(2))); + assertExpression( + Double.POSITIVE_INFINITY, + of(Double.MAX_VALUE).add(of(Double.MAX_VALUE))); + + // convenience + assertExpression(3.0, of(1.0).add(2.0)); + assertExpression(3L, of(1).add(2L)); + assertExpression(3, of(1).add(2)); + + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/ + // sum's alternative behaviour exists for purposes of reduction, but is + // inconsistent with multiply, and potentially confusing. Unimplemented. + } + + @Test + public void subtractTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/subtract/ + MqlInteger actual = of(2).subtract(of(2)); + assertExpression( + 0, + actual, + "{'$subtract': [2, 2]} "); + assertExpression( + 2.0 - 2, + of(2.0).subtract(of(2)), + "{'$subtract': [2.0, 2]} "); + + // convenience + assertExpression(-1.0, of(1.0).subtract(2.0)); + assertExpression(-1, of(1).subtract(2)); + } + + @Test + public void maxTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/max/ + MqlInteger actual = of(-2).max(of(2)); + assertExpression( + Math.max(-2, 2), + actual, + "{'$max': [-2, 2]}"); + assertExpression( + Math.max(-2.0, 2.0), + of(-2.0).max(of(2.0)), + "{'$max': [-2.0, 2.0]}"); + } + + @Test + public void minTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/min/ + MqlInteger actual = of(-2).min(of(2)); + assertExpression( + Math.min(-2, 2), + actual, + "{'$min': [-2, 2]}"); + assertExpression( + Math.min(-2.0, 2.0), + of(-2.0).min(of(2.0)), + "{'$min': [-2.0, 2.0]}"); + } + + @Test + public void roundTest() { + assumeTrue(serverVersionAtLeast(4, 2)); + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/round/ + MqlInteger actual = of(5.5).round(); + assertExpression( + 6.0, + actual, + "{'$round': 5.5} "); + MqlNumber actualNum = of(5.5).round(of(0)); + assertExpression( + new BigDecimal("5.5").setScale(0, RoundingMode.HALF_EVEN).doubleValue(), + actualNum, + "{'$round': [5.5, 0]} "); + // unlike Java, uses banker's rounding (half_even) + assertExpression( + 2.0, + of(2.5).round(), + "{'$round': 2.5} "); + assertExpression( + new BigDecimal("-5.5").setScale(0, RoundingMode.HALF_EVEN).doubleValue(), + of(-5.5).round()); + // to place + assertExpression( + 555.55, + of(555.555).round(of(2)), + "{'$round': [555.555, 2]} "); + assertExpression( + 600.0, + of(555.555).round(of(-2)), + "{'$round': [555.555, -2]} "); + // underlying type rounds to same underlying type + assertExpression( + 5L, + of(5L).round()); + assertExpression( + 5.0, + of(5.0).round()); + assertExpression( + Decimal128.parse("1234"), + of(Decimal128.parse("1234.2")).round()); + } + + @Test + public void absTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/round/ + assertExpression( + Math.abs(-2.0), + of(-2.0).abs(), + "{'$abs': -2.0}"); + // integer + MqlInteger abs = of(-2).abs(); + assertExpression( + Math.abs(-2), abs, + "{'$abs': -2}"); + } +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/ArrayMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/ArrayMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..59ca6027742 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/ArrayMqlValuesFunctionalTest.java @@ -0,0 +1,462 @@ +/* + * 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.mql; + +import com.mongodb.MongoCommandException; +import org.bson.Document; +import org.bson.types.Decimal128; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofArray; +import static com.mongodb.client.model.mql.MqlValues.ofBooleanArray; +import static com.mongodb.client.model.mql.MqlValues.ofDateArray; +import static com.mongodb.client.model.mql.MqlValues.ofIntegerArray; +import static com.mongodb.client.model.mql.MqlValues.ofNumberArray; +import static com.mongodb.client.model.mql.MqlValues.ofStringArray; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +@SuppressWarnings({"Convert2MethodRef"}) +class ArrayMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#array-expression-operators + + private final MqlArray array123 = ofIntegerArray(1, 2, 3); + private final MqlArray arrayTTF = ofBooleanArray(true, true, false); + + @Test + public void literalsTest() { + // Boolean + assertExpression( + Arrays.asList(true, true, false), + arrayTTF, + "[true, true, false]"); + // Integer + assertExpression( + Arrays.asList(1, 2, 3), + array123, + "[1, 2, 3]"); + assertExpression( + Arrays.asList(1L, 2L, 3L), + ofIntegerArray(1L, 2L, 3L), + "[{'$numberLong': '1'}, {'$numberLong': '2'}, {'$numberLong': '3'}]"); + // Number + assertExpression( + Arrays.asList(1.0, 2.0, 3.0), + ofNumberArray(1.0, 2.0, 3.0), + "[1.0, 2.0, 3.0]"); + assertExpression( + Arrays.asList(Decimal128.parse("1.0")), + ofNumberArray(Decimal128.parse("1.0")), + "[{'$numberDecimal': '1.0'}]"); + // String + assertExpression( + Arrays.asList("a", "b", "c"), + ofStringArray("a", "b", "c"), + "['a', 'b', 'c']"); + // Date + assertExpression( + 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 + MqlArray 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 + MqlArray> arrayArray = ofArray(ofArray(), ofArray()); + assertExpression( + Arrays.asList(Collections.emptyList(), Collections.emptyList()), + arrayArray, + "[[], []]"); + + // Mixed + MqlArray expression = ofArray(of(1), of(true), ofArray(of(1.0), of(1))); + assertExpression( + Arrays.asList(1, true, Arrays.asList(1.0, 1)), + expression, + "[1, true, [1.0, 1]]"); + } + + @Test + public void filterTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/ + assertExpression( + Stream.of(true, true, false) + .filter(v -> v).collect(Collectors.toList()), + arrayTTF.filter(v -> v), + "{'$filter': {'input': [true, true, false], 'cond': '$$this'}}"); + } + + @Test + public void mapTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/ + assertExpression( + Stream.of(true, true, false) + .map(v -> !v).collect(Collectors.toList()), + arrayTTF.map(v -> v.not()), + "{'$map': {'input': [true, true, false], 'in': {'$not': '$$this'}}}"); + } + + @Test + public void sortTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/ + MqlArray integerExpressionArrayExpression = ofIntegerArray(3, 1, 2); + assertExpression( + Stream.of(3, 1, 2) + .sorted().collect(Collectors.toList()), sort(integerExpressionArrayExpression), + "{'$sortArray': {'input': [3, 1, 2], 'sortBy': 1}}"); + } + + @SuppressWarnings("unchecked") + private static MqlArray sort(final MqlArray array) { + assumeTrue(serverVersionAtLeast(5, 2)); // due to sort + MqlExpression mqlArray = (MqlExpression) array; + return mqlArray.sort(); + } + + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/ + // reduce is implemented as each individual type of reduction (monoid) + // this prevents issues related to incorrect specification of identity values + + @Test + public void reduceAnyTest() { + assertExpression( + true, + arrayTTF.any(a -> a), + "{'$reduce': {'input': {'$map': {'input': [true, true, false], 'in': '$$this'}}, " + + "'initialValue': false, 'in': {'$or': ['$$value', '$$this']}}}"); + assertExpression( + false, + ofBooleanArray().any(a -> a)); + + assertExpression( + true, + ofIntegerArray(1, 2, 3).any(a -> a.eq(of(3)))); + assertExpression( + false, + ofIntegerArray(1, 2, 2).any(a -> a.eq(of(9)))); + } + + @Test + public void reduceAllTest() { + assertExpression( + false, + arrayTTF.all(a -> a), + "{'$reduce': {'input': {'$map': {'input': [true, true, false], 'in': '$$this'}}, " + + "'initialValue': true, 'in': {'$and': ['$$value', '$$this']}}}"); + assertExpression( + true, + ofBooleanArray().all(a -> a)); + + assertExpression( + true, + ofIntegerArray(1, 2, 3).all(a -> a.gt(of(0)))); + assertExpression( + false, + ofIntegerArray(1, 2, 2).all(a -> a.eq(of(2)))); + } + + @Test + public void reduceSumTest() { + assertExpression( + 6, + ofIntegerArray(1, 2, 3).sum(a -> a), + "{'$reduce': {'input': {'$map': {'input': [1, 2, 3], 'in': '$$this'}}, " + + "'initialValue': 0, 'in': {'$add': ['$$value', '$$this']}}}"); + // empty array: + assertExpression( + 0, + ofIntegerArray().sum(a -> a)); + } + + @Test + public void reduceMultiplyTest() { + assertExpression( + 6, + ofIntegerArray(1, 2, 3).multiply(a -> a), + "{'$reduce': {'input': {'$map': {'input': [1, 2, 3], 'in': '$$this'}}, " + + "'initialValue': 1, 'in': {'$multiply': ['$$value', '$$this']}}}"); + // empty array: + assertExpression( + 1, + ofIntegerArray().multiply(a -> a)); + } + + @Test + public void reduceMaxTest() { + assumeTrue(serverVersionAtLeast(5, 2)); + assertExpression( + 3, + ofIntegerArray(1, 2, 3).max(of(9)), + "{'$cond': [{'$eq': [{'$size': [[1, 2, 3]]}, 0]}, 9, " + + "{'$first': [{'$maxN': {'input': [1, 2, 3], 'n': 1}}]}]}"); + assertExpression( + 9, + ofIntegerArray().max(of(9))); + } + + @Test + public void reduceMinTest() { + assumeTrue(serverVersionAtLeast(5, 2)); + assertExpression( + 1, + ofIntegerArray(1, 2, 3).min(of(9)), + "{'$cond': [{'$eq': [{'$size': [[1, 2, 3]]}, 0]}, 9, " + + "{'$first': [{'$minN': {'input': [1, 2, 3], 'n': 1}}]}]}"); + assertExpression( + 9, + ofIntegerArray().min(of(9))); + } + + @Test + public void reduceMaxNTest() { + assumeTrue(serverVersionAtLeast(5, 2)); + assertExpression( + Arrays.asList(3, 2), + ofIntegerArray(3, 1, 2).maxN(of(2))); + assertExpression( + Arrays.asList(), + ofIntegerArray().maxN(of(2))); + // N must be non-zero + assertThrows(MongoCommandException.class, () -> assertExpression( + Arrays.asList(), + ofIntegerArray(3, 2, 1).maxN(of(0)))); + } + + @Test + public void reduceMinNTest() { + assumeTrue(serverVersionAtLeast(5, 2)); + assertExpression( + Arrays.asList(1, 2), + ofIntegerArray(3, 1, 2).minN(of(2))); + assertExpression( + Arrays.asList(), + ofIntegerArray().minN(of(2))); + // N must be non-zero + assertThrows(MongoCommandException.class, () -> assertExpression( + Arrays.asList(), + ofIntegerArray(3, 2, 1).minN(of(0)))); + } + + @Test + public void reduceJoinTest() { + assertExpression( + "abc", + ofStringArray("a", "b", "c").joinStrings(a -> a), + "{'$reduce': {'input': {'$map': {'input': ['a', 'b', 'c'], 'in': '$$this'}}, " + + "'initialValue': '', 'in': {'$concat': ['$$value', '$$this']}}}"); + assertExpression( + "", + ofStringArray().joinStrings(a -> a)); + } + + @Test + public void reduceConcatTest() { + assertExpression( + Arrays.asList(1, 2, 3, 4), + ofArray(ofIntegerArray(1, 2), ofIntegerArray(3, 4)).concatArrays(v -> v), + "{'$reduce': {'input': {'$map': {'input': [[1, 2], [3, 4]], 'in': '$$this'}}, " + + "'initialValue': [], " + + "'in': {'$concatArrays': ['$$value', '$$this']}}} "); + // empty: + MqlArray> expressionArrayExpression = ofArray(); + assertExpression( + Collections.emptyList(), + expressionArrayExpression.concatArrays(a -> a)); + } + + @Test + public void reduceUnionTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/setUnion/ (40) + assertExpression( + Arrays.asList(1, 2, 3), + sort(ofArray(ofIntegerArray(1, 2), ofIntegerArray(1, 3)).unionArrays(v -> v)), + "{'$sortArray': {'input': {'$reduce': {'input': " + + "{'$map': {'input': [[1, 2], [1, 3]], 'in': '$$this'}}, " + + "'initialValue': [], 'in': {'$setUnion': ['$$value', '$$this']}}}, 'sortBy': 1}}"); + + Function, MqlArray> f = a -> + a.map(v -> v.isBooleanOr(of(false)) + .cond(of(1), of(0))); + assertExpression( + Arrays.asList(0, 1), + ofArray(ofBooleanArray(true, false), ofBooleanArray(false)).unionArrays(f)); + } + + @Test + public void sizeTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/size/ + assertExpression( + Arrays.asList(1, 2, 3).size(), + array123.size(), + "{'$size': [[1, 2, 3]]}"); + assertExpression( + 0, + ofIntegerArray().size(), + "{'$size': [[]]}"); + } + + @Test + public void elementAtTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/arrayElemAt/ + assertExpression( + Arrays.asList(1, 2, 3).get(0), + array123.elementAt(of(0)), + "{'$arrayElemAt': [[1, 2, 3], 0]}"); + // negatives + assertExpression( + Arrays.asList(1, 2, 3).get(3 - 1), + array123.elementAt(-1)); + // underlying long + assertExpression( + 2, + array123.elementAt(of(1L))); + + assertExpression( + MISSING, + array123.elementAt(99)); + + assertExpression( + MISSING, + array123.elementAt(-99)); + + // long values are considered entirely out of bounds; server error + assertThrows(MongoCommandException.class, () -> assertExpression( + MISSING, + array123.elementAt(of(Long.MAX_VALUE)))); + + // 0.0 is a valid integer value + assumeTrue(serverVersionAtLeast(4, 4)); // isNumber + assertExpression( + Arrays.asList(1, 2, 3).get(0), + array123.elementAt(of(0.0).isIntegerOr(of(-1)))); + } + + @Test + public void firstTest() { + assumeTrue(serverVersionAtLeast(4, 4)); + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/first/ + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/first-array-element/ + assertExpression( + new LinkedList<>(Arrays.asList(1, 2, 3)).getFirst(), + array123.first(), + "{'$first': [[1, 2, 3]]}"); + + assertExpression( + MISSING, + ofIntegerArray().first(), + "{'$first': [[]]}"); + } + + @Test + public void lastTest() { + assumeTrue(serverVersionAtLeast(4, 4)); + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/last-array-element/ + assertExpression( + new LinkedList<>(Arrays.asList(1, 2, 3)).getLast(), + array123.last(), + "{'$last': [[1, 2, 3]]}"); + + assertExpression( + MISSING, + ofIntegerArray().last(), + "{'$last': [[]]}"); + } + + @Test + public void containsTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/in/ + // The parameters of this expression are flipped + assertExpression( + Arrays.asList(1, 2, 3).contains(2), + array123.contains(of(2)), + "{'$in': [2, [1, 2, 3]]}"); + } + + @Test + public void concatTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/concatArrays/ + assertExpression( + Stream.concat(Stream.of(1, 2, 3), Stream.of(1, 2, 3)) + .collect(Collectors.toList()), + ofIntegerArray(1, 2, 3).concat(ofIntegerArray(1, 2, 3)), + "{'$concatArrays': [[1, 2, 3], [1, 2, 3]]}"); + // mixed types: + assertExpression( + Arrays.asList(1.0, 1, 2, 3), + ofNumberArray(1.0).concat(ofIntegerArray(1, 2, 3))); + } + + @Test + public void sliceTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/slice/ + assertExpression( + Arrays.asList(1, 2, 3).subList(1, 3), + array123.slice(1, 10), + "{'$slice': [[1, 2, 3], 1, 10]}"); + + MqlArray array12345 = ofIntegerArray(1, 2, 3, 4, 5); + // sub-array: skipFirstN + firstN + assertExpression( + Arrays.asList(2, 3), + array12345.slice(1, 2)); + // lastN + firstN + assertExpression( + Arrays.asList(5), + array12345.slice(-1, 100)); + assertExpression( + Arrays.asList(1, 2, 3, 4, 5), + array12345.slice(-100, 100)); + } + + @Test + public void unionTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/setUnion/ + assertExpression( + Arrays.asList(1, 2, 3), + sort(array123.union(array123)), + "{'$sortArray': {'input': {'$setUnion': [[1, 2, 3], [1, 2, 3]]}, 'sortBy': 1}}"); + // mixed types: + assertExpression( + Arrays.asList(1, 2.0, 3), + sort(ofNumberArray(2.0).union(ofIntegerArray(1, 2, 3)))); + } + + @Test + public void distinctTest() { + assertExpression( + Arrays.asList(1, 2, 3), + sort(ofIntegerArray(1, 2, 1, 3, 3).distinct()), + "{'$sortArray': {'input': {'$setUnion': [[1, 2, 1, 3, 3]]}, 'sortBy': 1}}"); + } +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/BooleanMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/BooleanMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..0c5b8bd48ce --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/BooleanMqlValuesFunctionalTest.java @@ -0,0 +1,75 @@ +/* + * 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.mql; + +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"PointlessBooleanExpression", "ConstantConditions", "ConstantConditionalExpression", "SimplifyBooleanExpression"}) +class BooleanMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#boolean-expression-operators + // (Complete as of 6.0) + + private final MqlBoolean tru = MqlValues.of(true); + private final MqlBoolean fal = MqlValues.of(false); + + @Test + public void literalsTest() { + assertExpression(true, tru, "true"); + assertExpression(false, fal, "false"); + } + + @Test + public void orTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/or/ + assertExpression(true || false, tru.or(fal), "{'$or': [true, false]}"); + assertExpression(false || true, fal.or(tru), "{'$or': [false, true]}"); + } + + @Test + public void andTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/and/ + assertExpression(true && false, tru.and(fal), "{'$and': [true, false]}"); + assertExpression(false && true, fal.and(tru), "{'$and': [false, true]}"); + } + + @Test + public void notTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/not/ + assertExpression(!true, tru.not(), "{'$not': true}"); + assertExpression(!false, fal.not(), "{'$not': false}"); + } + + @Test + public void condTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/ + MqlString abc = MqlValues.of("abc"); + MqlString xyz = MqlValues.of("xyz"); + MqlNumber nnn = MqlValues.of(123); + assertExpression( + true && false ? "abc" : "xyz", + tru.and(fal).cond(abc, xyz), + "{'$cond': [{'$and': [true, false]}, 'abc', 'xyz']}"); + assertExpression( + true || false ? "abc" : "xyz", + tru.or(fal).cond(abc, xyz), + "{'$cond': [{'$or': [true, false]}, 'abc', 'xyz']}"); + assertExpression( + false ? "abc" : 123, + fal.cond(abc, nnn), + "{'$cond': [false, 'abc', 123]}"); + } +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/ComparisonMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/ComparisonMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..f5108fe4e25 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/ComparisonMqlValuesFunctionalTest.java @@ -0,0 +1,194 @@ +/* + * 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.mql; + +import org.bson.BsonDocument; +import org.bson.BsonValue; +import org.bson.codecs.BsonValueCodecProvider; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofBooleanArray; +import static com.mongodb.client.model.mql.MqlValues.ofIntegerArray; +import static com.mongodb.client.model.mql.MqlValues.ofNull; +import static org.bson.codecs.configuration.CodecRegistries.fromProviders; +import static org.junit.jupiter.api.Assertions.fail; + +@SuppressWarnings({"ConstantConditions"}) +class ComparisonMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#comparison-expression-operators + // (Complete as of 6.0) + // 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( + MqlExpression.ofRem(), + ofNull(), + of(0), + of(1), + of(2.0), + of(""), + of("str"), + of(BsonDocument.parse("{}")), + of(BsonDocument.parse("{a: 1}")), + of(BsonDocument.parse("{a: 2}")), + of(BsonDocument.parse("{a: 2, b: 1}")), + of(BsonDocument.parse("{b: 1, a: 2}")), + of(BsonDocument.parse("{'':''}")), + ofIntegerArray(0), + ofIntegerArray(1), + ofBooleanArray(true), + of(false), + of(true), + 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/ + assertExpression( + 1 == 2, + of(1).eq(of(2)), + "{'$eq': [1, 2]}"); + assertExpression( + false, + of(BsonDocument.parse("{}")).eq(ofIntegerArray()), + "{'$eq': [{'$literal': {}}, []]}"); + + // numbers are equal, even though of different types + assertExpression( + 1 == 1.0, + of(1).eq(of(1.0)), + "{'$eq': [1, 1.0]}"); + assertExpression( + 1 == 1L, + of(1).eq(of(1L)), + "{'$eq': [1, { '$numberLong': '1' }]}"); + + // ensure that no two samples are equal to each other + for (int i = 0; i < sampleValues.size(); i++) { + for (int j = 0; j < sampleValues.size(); j++) { + if (i == j) { + continue; + } + MqlValue first = sampleValues.get(i); + MqlValue second = sampleValues.get(j); + BsonValue evaluate = evaluate(first.eq(second)); + if (evaluate.asBoolean().getValue()) { + BsonValue v1 = ((MqlExpression) first).toBsonValue(fromProviders(new BsonValueCodecProvider())); + BsonValue v2 = ((MqlExpression) second).toBsonValue(fromProviders(new BsonValueCodecProvider())); + fail(i + "," + j + " --" + v1 + " and " + v2 + " should not equal"); + } + } + } + } + + @Test + public void neTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/ne/ + assertExpression( + 1 != 2, + of(1).ne(of(2)), + "{'$ne': [1, 2]}"); + } + + @Test + public void ltTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/lt/ + assertExpression( + -1 < 1, + of(-1).lt(of(1)), + "{'$lt': [-1, 1]}"); + assertExpression( + 0 < 0, + of(0).lt(of(0)), + "{'$lt': [0, 0]}"); + + assertExpression( + true, + ofNull().lt(of(0)), + "{'$lt': [null, 0]}"); + + for (int i = 0; i < sampleValues.size() - 1; i++) { + for (int j = i + 1; j < sampleValues.size(); j++) { + MqlValue first = sampleValues.get(i); + MqlValue second = sampleValues.get(j); + BsonValue evaluate = evaluate(first.lt(second)); + if (!evaluate.asBoolean().getValue()) { + BsonValue v1 = ((MqlExpression) first).toBsonValue(fromProviders(new BsonValueCodecProvider())); + BsonValue v2 = ((MqlExpression) second).toBsonValue(fromProviders(new BsonValueCodecProvider())); + fail(i + "," + j + " --" + v1 + " < " + v2 + " should be true"); + } + } + } + } + + @Test + public void lteTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/lte/ + assertExpression( + -1 <= 1, + of(-1).lte(of(1)), + "{'$lte': [-1, 1]}"); + assertExpression( + 0 <= 0, + of(0).lte(of(0)), + "{'$lte': [0, 0]}"); + } + + @Test + public void gtTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/gt/ + assertExpression( + -1 > 1, + of(-1).gt(of(1)), + "{'$gt': [-1, 1]}"); + assertExpression( + 0 > 0, + of(0).gt(of(0)), + "{'$gt': [0, 0]}"); + } + + @Test + public void gteTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/gte/ + assertExpression( + -1 >= 1, + of(-1).gte(of(1)), + "{'$gte': [-1, 1]}"); + assertExpression( + 0 >= 0, + of(0).gte(of(0)), + "{'$gte': [0, 0]}"); + } + +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/ControlMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/ControlMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..706f20c2e60 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/ControlMqlValuesFunctionalTest.java @@ -0,0 +1,292 @@ +/* + * 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.mql; + +import org.bson.Document; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.function.Function; + +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofArray; +import static com.mongodb.client.model.mql.MqlValues.ofIntegerArray; +import static com.mongodb.client.model.mql.MqlValues.ofMap; +import static com.mongodb.client.model.mql.MqlValues.ofNull; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class ControlMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + + @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() { + assumeTrue(serverVersionAtLeast(4, 4)); // isNumber + // 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() { + // isIntegerOr relies on switch short-circuiting, which only happens after 5.2 + assumeTrue(serverVersionAtLeast(5, 2)); + Function label = expr -> expr.switchOn(on -> on + .isBoolean(v -> v.asString().append(of(" - bool"))) + // integer should be checked before string + .isInteger(v -> v.asString().append(of(" - integer"))) + .isNumber(v -> v.asString().append(of(" - number"))) + .isString(v -> v.asString().append(of(" - string"))) + .isDate(v -> v.asString().append(of(" - date"))) + .isArray((MqlArray v) -> v.sum(a -> a).asString().append(of(" - array"))) + .isDocument(v -> v.getString("_id").append(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((MqlMap v) -> v.entries() + .joinStrings(e -> e.getValue()).append(of(" - map"))))); + // arrays via isArray, and tests signature: + assertExpression( + "ab - array", + ofArray(of("a"), of("b")).switchOn(on -> on + .isArray((MqlArray v) -> v + .joinStrings(e -> e).append(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("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 switchTestInitialVersion44() { + assumeTrue(serverVersionAtLeast(4, 4)); + 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'}]}}"); + } + @Test + public void switchTestPartialVersion44() { + assumeTrue(serverVersionAtLeast(4, 4)); + 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", + 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'}]}}"); + } + + @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("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'}]}}"); + } +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/DateMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/DateMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..2683b772bf2 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/DateMqlValuesFunctionalTest.java @@ -0,0 +1,138 @@ +/* + * 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.mql; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; + +import static com.mongodb.client.model.mql.MqlValues.of; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SuppressWarnings("ConstantConditions") +class DateMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#date-expression-operators + + private final Instant instant = Instant.parse("2007-12-03T10:15:30.005Z"); + private final MqlDate date = of(instant); + private final ZonedDateTime utcDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of(ZoneOffset.UTC.getId())); + private final MqlString utc = of("UTC"); + + @Test + public void literalsTest() { + assertExpression( + instant, + date, + "{'$date': '2007-12-03T10:15:30.005Z'}"); + assertThrows(IllegalArgumentException.class, () -> of((Instant) null)); + } + + @Test + public void yearTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/year/ + assertExpression( + utcDateTime.get(ChronoField.YEAR), + date.year(utc), + "{'$year': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + + @Test + public void monthTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/month/ + assertExpression( + utcDateTime.get(ChronoField.MONTH_OF_YEAR), + date.month(utc), + "{'$month': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + + @Test + public void dayOfMonthTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfMonth/ + assertExpression( + utcDateTime.get(ChronoField.DAY_OF_MONTH), + date.dayOfMonth(utc), + "{'$dayOfMonth': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + + @Test + public void dayOfWeekTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfWeek/ + assertExpression( + utcDateTime.get(ChronoField.DAY_OF_WEEK) + 1, + date.dayOfWeek(utc), + "{'$dayOfWeek': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + + @Test + public void dayOfYearTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfYear/ + assertExpression( + utcDateTime.get(ChronoField.DAY_OF_YEAR), + date.dayOfYear(utc), + "{'$dayOfYear': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + + @Test + public void hourTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/hour/ + assertExpression( + utcDateTime.get(ChronoField.HOUR_OF_DAY), + date.hour(utc), + "{'$hour': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + + @Test + public void minuteTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/minute/ + assertExpression( + utcDateTime.get(ChronoField.MINUTE_OF_HOUR), + date.minute(utc), + "{'$minute': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + + @Test + public void secondTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/second/ + assertExpression( + utcDateTime.get(ChronoField.SECOND_OF_MINUTE), + date.second(utc), + "{'$second': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + + @Test + public void weekTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/week/ + assertExpression( + 48, + date.week(utc), + "{'$week': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + + @Test + public void millisecondTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/millisecond/ + assertExpression( + utcDateTime.get(ChronoField.MILLI_OF_SECOND), + date.millisecond(utc), + "{'$millisecond': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); + } + +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/DocumentMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/DocumentMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..9afc7274953 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/DocumentMqlValuesFunctionalTest.java @@ -0,0 +1,283 @@ +/* + * 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.mql; + +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.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofIntegerArray; +import static com.mongodb.client.model.mql.MqlValues.ofMap; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +@SuppressWarnings("ConstantConditions") +class DocumentMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#object-expression-operators + // (Complete as of 6.0) + + private static MqlDocument ofDoc(final String ofDoc) { + return of(BsonDocument.parse(ofDoc)); + } + + private final MqlDocument a1 = ofDoc("{a: 1}"); + private final MqlDocument 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() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + // 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 + MqlDocument 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() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + // 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}"))); + assertExpression(Document.parse("{b: 2}"), ofDoc("{a: {b: 2}}") + .getMap("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}")))); + assertExpression(Document.parse("{b: 2}"), ofDoc("{a: {b: 2}}") + .getMap("a", ofMap(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}"))); + assertExpression(Document.parse("{z: 99}"), ofDoc("{}") + .getMap("a", Document.parse("{z: 99}"))); + + // int vs num + assertExpression(99, ofDoc("{a: 1.1}").getInteger("a", of(99))); + } + + @Test + public void getFieldMissingTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + // 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() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + // 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)), + "{'$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", MqlValues.ofNull()), + "{'$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)), + "{'$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)), + "{'$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() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField (unset) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/unsetField/ + assertExpression( + BsonDocument.parse("{}"), // map.remove("a") + a1.unsetField("a"), + "{'$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}"))); + } + + @Test + public void asMapTest() { + MqlDocument d = ofDoc("{a: 1}"); + assertSame(d, d.asMap()); + } + + + @Test + public void hasTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + MqlDocument d = ofDoc("{a: 1, null: null}"); + assertExpression( + true, + d.hasField("a"), + "{'$ne': [{'$getField': {'input': {'$literal': {'a': 1, 'null': null}}, 'field': 'a'}}, '$$REMOVE']}"); + assertExpression( + false, + d.hasField("not_a")); + assertExpression( + true, + d.hasField("null")); + } +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/MapMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/MapMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..b85100e4e05 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/MapMqlValuesFunctionalTest.java @@ -0,0 +1,205 @@ +/* + * 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.mql; + +import org.bson.BsonDocument; +import org.bson.Document; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofArray; +import static com.mongodb.client.model.mql.MqlValues.ofEntry; +import static com.mongodb.client.model.mql.MqlValues.ofMap; +import static com.mongodb.client.model.mql.MqlValues.ofStringArray; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class MapMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + + private final MqlMap mapKey123 = MqlValues.ofMap() + .set("key", of(123)); + + private final MqlMap mapA1B2 = ofMap(Document.parse("{keyA: 1, keyB: 2}")); + + @Test + public void literalsTest() { + // entry + assertExpression( + Document.parse("{k: 'keyA', v: 1}"), + ofEntry(of("keyA"), of(1))); + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField (unset) + // map + assertExpression( + Document.parse("{keyA: 1, keyB: 2}"), + ofMap(Document.parse("{keyA: 1, keyB: 2}")), + "{'$literal': {'keyA': 1, 'keyB': 2}}"); + assertExpression( + Document.parse("{key: 123}"), + mapKey123, + "{'$setField': {'field': 'key', 'input': {'$literal': {}}, 'value': 123}}"); + } + + @Test + public void getSetMapTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + // get + assertExpression( + 123, + mapKey123.get("key")); + assertExpression( + 1, + mapKey123.get("missing", of(1))); + // set (map.put) + assertExpression( + BsonDocument.parse("{key: 123, b: 1}"), + mapKey123.set("b", of(1))); + // unset (delete) + assertExpression( + BsonDocument.parse("{}"), + mapKey123.unset("key")); + // "other" parameter + assertExpression( + null, + ofMap(Document.parse("{ 'null': null }")).get("null", of(1))); + } + + @Test + public void hasTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField (unset) + MqlMap e = ofMap(BsonDocument.parse("{key: 1, null: null}")); + assertExpression( + true, + e.has(of("key")), + "{'$ne': [{'$getField': {'input': {'$literal': {'key': 1, 'null': null}}, 'field': 'key'}}, '$$REMOVE']}"); + assertExpression( + false, + e.has("not_key")); + assertExpression( + true, + e.has("null")); + // consistency: + assertExpression(true, e.has("null")); + assertExpression(null, e.get("null", of(1))); + } + + @Test + public void getSetEntryTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + MqlEntry entryA1 = ofEntry(of("keyA"), of(1)); + assertExpression( + Document.parse("{k: 'keyA', 'v': 33}"), + entryA1.setValue(of(33))); + assertExpression( + Document.parse("{k: 'keyB', 'v': 1}"), + entryA1.setKey(of("keyB"))); + } + + @Test + public void buildMapTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/arrayToObject/ (48) + assertExpression( + Document.parse("{'keyA': 1}"), + ofArray(ofEntry(of("keyA"), of(1))).asMap(v -> v), + "{'$arrayToObject': [{'$map': {'input': [{'k': 'keyA', 'v': 1}], 'in': '$$this'}}]}"); + + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + assertExpression( + Document.parse("{'keyA': 55}"), + ofArray(ofEntry(of("keyA"), of(1))).asMap(v -> v.setValue(of(55))), + "{'$arrayToObject': [{'$map': {'input': [{'k': 'keyA', 'v': 1}], " + + "'in': {'$setField': {'field': 'v', 'input': '$$this', 'value': 55}}}}]}"); + + // using documents + assertExpression( + Document.parse("{ 'item' : 'abc123', 'qty' : 25 }"), + ofArray( + of(Document.parse("{ 'k': 'item', 'v': 'abc123' }")), + of(Document.parse("{ 'k': 'qty', 'v': 25 }"))) + .asMap(v -> ofEntry(v.getString("k"), v.getField("v")))); + // using arrays + assertExpression( + Document.parse("{ 'item' : 'abc123', 'qty' : 25 }"), + ofArray( + ofStringArray("item", "abc123"), + ofArray(of("qty"), of(25))) + .asMap(v -> ofEntry(v.elementAt(of(0)).asString(), v.elementAt(of(1))))); + // last listed value used + assertExpression( + Document.parse("{ 'item' : 'abc123' }"), + ofArray( + MqlValues.ofMap(Document.parse("{ 'k': 'item', 'v': '123abc' }")), + MqlValues.ofMap(Document.parse("{ 'k': 'item', 'v': 'abc123' }"))) + .asMap(v -> ofEntry(v.get("k"), v.get("v")))); + + } + + @Test + public void entrySetTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/objectToArray/ (23) + assertExpression( + Arrays.asList(Document.parse("{'k': 'k1', 'v': 1}")), + MqlValues.ofMap().set("k1", of(1)).entries(), + "{'$objectToArray': {'$setField': " + + "{'field': 'k1', 'input': {'$literal': {}}, 'value': 1}}}"); + + // key/value usage + assertExpression( + "keyA|keyB|", + mapA1B2.entries().map(v -> v.getKey().append(of("|"))).joinStrings(v -> v)); + assertExpression( + 23, + mapA1B2.entries().map(v -> v.getValue().add(10)).sum(v -> v)); + + // combined entrySet-buildMap usage + assertExpression( + Document.parse("{'keyA': 2, 'keyB': 3}"), + mapA1B2 + .entries() + .map(v -> v.setValue(v.getValue().add(1))) + .asMap(v -> v)); + + // via getMap + MqlDocument doc = of(Document.parse("{ instock: { warehouse1: 2500, warehouse2: 500 } }")); + assertExpression( + Arrays.asList( + Document.parse("{'k': 'warehouse1', 'v': 2500}"), + Document.parse("{'k': 'warehouse2', 'v': 500}")), + doc.getMap("instock").entries(), + "{'$objectToArray': {'$getField': {'input': {'$literal': " + + "{'instock': {'warehouse1': 2500, 'warehouse2': 500}}}, 'field': 'instock'}}}"); + } + + @Test + public void mergeTest() { + assertExpression( + Document.parse("{'keyA': 9, 'keyB': 2, 'keyC': 3}"), + ofMap(Document.parse("{keyA: 1, keyB: 2}")) + .merge(ofMap(Document.parse("{keyA: 9, keyC: 3}"))), + "{'$mergeObjects': [{'$literal': {'keyA': 1, 'keyB': 2}}, " + + "{'$literal': {'keyA': 9, 'keyC': 3}}]}"); + } + + @Test + public void asDocumentTest() { + MqlMap d = ofMap(BsonDocument.parse("{a: 1}")); + assertSame(d, d.asDocument()); + } +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/NotNullApiTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/NotNullApiTest.java new file mode 100644 index 00000000000..97635bbf44d --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/NotNullApiTest.java @@ -0,0 +1,164 @@ +/* + * 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.mql; + +import org.bson.BsonDocument; +import org.bson.conversions.Bson; +import org.bson.types.Decimal128; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static com.mongodb.assertions.Assertions.fail; +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofArray; +import static com.mongodb.client.model.mql.MqlValues.ofEntry; +import static com.mongodb.client.model.mql.MqlValues.ofMap; +import static com.mongodb.client.model.mql.MqlValues.ofNull; + +class NotNullApiTest { + + @Test + public void notNullApiTest() { + Map, Object> mapping = new HashMap<>(); + Map, Object> paramMapping = new HashMap<>(); + + // to test: + mapping.put(MqlValues.class, null); + mapping.put(MqlBoolean.class, of(true)); + mapping.put(MqlInteger.class, of(1)); + mapping.put(MqlNumber.class, of(1.0)); + mapping.put(MqlString.class, of("")); + mapping.put(MqlDate.class, of(Instant.now())); + mapping.put(MqlDocument.class, of(BsonDocument.parse("{}"))); + mapping.put(MqlMap.class, ofMap(BsonDocument.parse("{}"))); + mapping.put(MqlArray.class, ofArray()); + mapping.put(MqlValue.class, ofNull()); + mapping.put(MqlEntry.class, ofEntry(of(""), of(""))); + mapping.put(Branches.class, new Branches<>()); + mapping.put(BranchesIntermediary.class, new BranchesIntermediary<>(Collections.emptyList())); + mapping.put(BranchesTerminal.class, new BranchesTerminal<>(Collections.emptyList(), null)); + + // additional params from classes not tested: + paramMapping.put(String.class, ""); + paramMapping.put(Instant.class, Instant.now()); + paramMapping.put(Bson.class, BsonDocument.parse("{}")); + paramMapping.put(Function.class, Function.identity()); + paramMapping.put(Number.class, 1); + paramMapping.put(int.class, 1); + paramMapping.put(boolean.class, true); + paramMapping.put(long.class, 1L); + paramMapping.put(Object.class, new Object()); + paramMapping.put(Decimal128.class, new Decimal128(1)); + putArray(paramMapping, MqlValue.class); + putArray(paramMapping, boolean.class); + putArray(paramMapping, long.class); + putArray(paramMapping, int.class); + putArray(paramMapping, double.class); + putArray(paramMapping, Decimal128.class); + putArray(paramMapping, Instant.class); + putArray(paramMapping, String.class); + + checkNotNullApi(mapping, paramMapping); + } + + private void putArray(final Map, Object> paramMapping, final Class componentType) { + final Object o = Array.newInstance(componentType, 0); + paramMapping.put(o.getClass(), o); + } + + private void checkNotNullApi( + final Map, Object> mapping, + final Map, Object> paramMapping) { + Map, Object> allParams = new HashMap<>(); + allParams.putAll(mapping); + allParams.putAll(paramMapping); + List uncheckedMethods = new ArrayList<>(); + for (Map.Entry, Object> entry : mapping.entrySet()) { + Object instance = entry.getValue(); + Class clazz = entry.getKey(); + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (!Modifier.isPublic(method.getModifiers())) { + continue; + } + boolean failed = false; + for (int i = 0; i < method.getParameterCount(); i++) { + if (method.getParameterTypes()[i].isPrimitive()) { + continue; + } + if (method.toString().endsWith(".equals(java.lang.Object)")) { + continue; + } + Object[] args = createArgs(allParams, method); + args[i] = null; // set one parameter to null + try { + // the method needs to throw due to Assertions.notNull: + method.invoke(instance, args); + failed = true; + } catch (Exception e) { + Throwable cause = e.getCause(); + if (!(cause instanceof IllegalArgumentException)) { + failed = true; + continue; + } + StackTraceElement[] trace = cause.getStackTrace(); + if (!method.getName().equals(trace[1].getMethodName())) { + failed = true; + } + if (!"notNull".equals(trace[0].getMethodName())) { + failed = true; + } + } + } + if (failed) { + uncheckedMethods.add("> " + method); + } + } + } + if (uncheckedMethods.size() > 0) { + fail("Assertions.notNull must be called on parameter from " + + uncheckedMethods.size() + " methods:\n" + + String.join("\n", uncheckedMethods)); + } + } + + private Object[] createArgs(final Map, ?> mapping, final Method method) { + Object[] args = new Object[method.getParameterCount()]; + Class[] parameterTypes = method.getParameterTypes(); + for (int j = 0; j < parameterTypes.length; j++) { + Class p = parameterTypes[j]; + Object arg = mapping.get(p); + if (arg == null) { + throw new IllegalArgumentException("mappings did not contain parameter of type: " + + p + " for method " + method); + } + args[j] = arg; + } + return args; + } + +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/StringMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/StringMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..34e39b9d483 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/StringMqlValuesFunctionalTest.java @@ -0,0 +1,172 @@ +/* + * 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.mql; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static com.mongodb.client.model.mql.MqlValues.of; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SuppressWarnings({"ConstantConditions"}) +class StringMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#string-expression-operators + + private final String jalapeno = "jalape\u00F1o"; + private final String sushi = "\u5BFF\u53F8"; + private final String fish = "\uD83D\uDC1F"; + + @Test + public void literalsTest() { + assertExpression("", of(""), "''"); + assertExpression("abc", of("abc"), "'abc'"); + assertThrows(IllegalArgumentException.class, () -> of((String) null)); + assertExpression(fish, of(fish), "'" + fish + "'"); + } + + @Test + public void concatTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/concat/ + assertExpression( + "abc".concat("de"), + of("abc").append(of("de")), + "{'$concat': ['abc', 'de']}"); + } + + @Test + public void toLowerTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLower/ + assertExpression( + "ABC".toLowerCase(), + of("ABC").toLower(), + "{'$toLower': 'ABC'}"); + } + + @Test + public void toUpperTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toUpper/ + assertExpression( + "abc".toUpperCase(), + of("abc").toUpper(), + "{'$toUpper': 'abc'}"); + } + + @Test + public void strLenTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenCP/ + assertExpression( + "abc".codePointCount(0, 3), + of("abc").length(), + "{'$strLenCP': 'abc'}"); + + // unicode + assertExpression( + jalapeno.codePointCount(0, jalapeno.length()), + of(jalapeno).length(), + "{'$strLenCP': '" + jalapeno + "'}"); + assertExpression( + sushi.codePointCount(0, sushi.length()), + of(sushi).length(), + "{'$strLenCP': '" + sushi + "'}"); + assertExpression( + fish.codePointCount(0, fish.length()), + of(fish).length(), + "{'$strLenCP': '" + fish + "'}"); + } + + @Test + public void strLenBytesTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenBytes/ + assertExpression( + "abc".getBytes(StandardCharsets.UTF_8).length, + of("abc").lengthBytes(), + "{'$strLenBytes': 'abc'}"); + + // unicode + assertExpression( + jalapeno.getBytes(StandardCharsets.UTF_8).length, + of(jalapeno).lengthBytes(), + "{'$strLenBytes': '" + jalapeno + "'}"); + assertExpression( + sushi.getBytes(StandardCharsets.UTF_8).length, + of(sushi).lengthBytes(), + "{'$strLenBytes': '" + sushi + "'}"); + assertExpression( + fish.getBytes(StandardCharsets.UTF_8).length, + of(fish).lengthBytes(), + "{'$strLenBytes': '" + fish + "'}"); + + // comparison + assertExpression(8, of(jalapeno).length()); + assertExpression(9, of(jalapeno).lengthBytes()); + assertExpression(2, of(sushi).length()); + assertExpression(6, of(sushi).lengthBytes()); + assertExpression(1, of(fish).length()); + assertExpression(4, of(fish).lengthBytes()); + } + + @Test + public void substrTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/substr/ + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/ + // substr is deprecated, an alias for bytes + assertExpression( + "abc".substring(1, 1 + 1), + of("abc").substr(of(1), of(1)), + "{'$substrCP': ['abc', 1, 1]}"); + assertExpression( + "bc", + of("abc").substr(of(1), of(100)), + "{'$substrCP': ['abc', 1, 100]}"); + + // unicode + assertExpression( + jalapeno.substring(5, 5 + 3), + of(jalapeno).substr(of(5), of(3)), + "{'$substrCP': ['" + jalapeno + "', 5, 3]}"); + assertExpression( + "e\u00F1o", + of(jalapeno).substr(of(5), of(3))); + + // bounds; convenience + assertExpression("abc", of("abc").substr(0, 99)); + assertExpression("ab", of("abc").substr(0, 2)); + assertExpression("b", of("abc").substr(1, 1)); + assertExpression("", of("abc").substr(1, 0)); + } + + @Test + public void substrBytesTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/ + assertExpression( + "b", + of("abc").substrBytes(of(1), of(1)), + "{'$substrBytes': ['abc', 1, 1]}"); + + // unicode + byte[] bytes = Arrays.copyOfRange(sushi.getBytes(StandardCharsets.UTF_8), 0, 3); + String expected = new String(bytes, StandardCharsets.UTF_8); + assertExpression(expected, + of(sushi).substrBytes(of(0), of(3))); + // server returns "starting index is a UTF-8 continuation byte" error when substrBytes(1, 1) + + // convenience + assertExpression("b", of("abc").substrBytes(1, 1)); + } +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..7df9748da1f --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java @@ -0,0 +1,326 @@ +/* + * 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.mql; + +import com.mongodb.MongoCommandException; +import org.bson.BsonDocument; +import org.bson.Document; +import org.bson.types.Decimal128; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; + +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofIntegerArray; +import static com.mongodb.client.model.mql.MqlValues.ofMap; +import static com.mongodb.client.model.mql.MqlValues.ofNull; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class TypeMqlValuesFunctionalTest extends AbstractMqlValuesFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#type-expression-operators + + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/ + // type is not implemented directly; instead, similar checks done via switch + + @Test + public void isBooleanOrTest() { + assertExpression( + true, + of(true).isBooleanOr(of(false)), + "{'$cond': [{'$eq': [{'$type': [true]}, 'bool']}, true, false]}"); + // non-boolean: + assertExpression(false, ofIntegerArray(1).isBooleanOr(of(false))); + assertExpression(false, ofNull().isBooleanOr(of(false))); + } + + @Test + public void isNumberOrTest() { + assumeTrue(serverVersionAtLeast(4, 4)); + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/ + assertExpression(1, of(1).isNumberOr(of(99)), "{'$cond': [{'$isNumber': [1]}, 1, 99]}"); + // other numeric values: + assertExpression(1L, of(1L).isNumberOr(of(99))); + assertExpression(1.0, of(1.0).isNumberOr(of(99))); + assertExpression(Decimal128.parse("1"), of(Decimal128.parse("1")).isNumberOr(of(99))); + // non-numeric: + assertExpression(99, ofIntegerArray(1).isNumberOr(of(99))); + assertExpression(99, ofNull().isNumberOr(of(99))); + } + + @Test + public void isIntegerOr() { + // isIntegerOr relies on switch short-circuiting, which only happens after 5.2 + assumeTrue(serverVersionAtLeast(5, 2)); + 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']}"); + // non-string: + assertExpression("or", ofIntegerArray(1).isStringOr(of("or"))); + assertExpression("or", ofNull().isStringOr(of("or"))); + } + + @Test + public void isDateOrTest() { + Instant date = Instant.parse("2007-12-03T10:15:30.005Z"); + assertExpression( + date, + of(date).isDateOr(of(date.plusMillis(10))), + "{'$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))); + assertExpression(date, ofNull().isDateOr(of(date))); + } + + @Test + public void isArrayOrTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/isArray/ + assertExpression( + Arrays.asList(1, 2), + ofIntegerArray(1, 2).isArrayOr(ofIntegerArray(99)), + "{'$cond': [{'$isArray': [[1, 2]]}, [1, 2], [99]]}"); + // non-array: + assertExpression(Arrays.asList(1, 2), of(true).isArrayOr(ofIntegerArray(1, 2))); + assertExpression(Arrays.asList(1, 2), ofNull().isArrayOr(ofIntegerArray(1, 2))); + } + + @Test + public void isDocumentOrTest() { + BsonDocument doc = BsonDocument.parse("{a: 1}"); + assertExpression( + doc, + of(doc).isDocumentOr(of(BsonDocument.parse("{b: 2}"))), + "{'$cond': [{'$eq': [{'$type': [{'$literal': {'a': 1}}]}, 'object']}, " + + "{'$literal': {'a': 1}}, {'$literal': {'b': 2}}]}"); + // non-document: + assertExpression(doc, ofIntegerArray(1).isDocumentOr(of(doc))); + assertExpression(doc, ofNull().isDocumentOr(of(doc))); + + // maps are documents + assertExpression(doc, ofMap(doc).isDocumentOr(of(BsonDocument.parse("{x: 9}")))); + + // conversion between maps and documents + MqlMap first = ofMap(doc); + MqlDocument second = first.isDocumentOr(of(BsonDocument.parse("{}"))); + MqlMap third = second.isMapOr(ofMap(BsonDocument.parse("{}"))); + assertExpression( + true, + first.eq(second)); + assertExpression( + true, + second.eq(third)); + } + + @Test + public void isMapOrTest() { + BsonDocument map = BsonDocument.parse("{a: 1}"); + assertExpression( + map, + ofMap(map).isMapOr(ofMap(BsonDocument.parse("{b: 2}"))), + "{'$cond': [{'$eq': [{'$type': [{'$literal': {'a': 1}}]}, 'object']}, " + + "{'$literal': {'a': 1}}, {'$literal': {'b': 2}}]}"); + // non-map: + assertExpression(map, ofIntegerArray(1).isMapOr(ofMap(map))); + assertExpression(map, ofNull().isMapOr(ofMap(map))); + + // documents are maps + assertExpression(map, of(map).isMapOr(ofMap(BsonDocument.parse("{x: 9}")))); + } + + // conversions + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/convert/ + // Convert is not implemented: too dynamic, conversions should be explicit. + + @Test + public void asStringTest() { + assumeTrue(serverVersionAtLeast(4, 0)); + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toString/ + // asString, since toString conflicts + assertExpression("false", of(false).asString(), "{'$toString': [false]}"); + + assertExpression("1", of(1).asString()); + assertExpression("1", of(1L).asString()); + assertExpression("1", of(1.0).asString()); + assertExpression("1.0", of(Decimal128.parse("1.0")).asString()); + + assertExpression("abc", of("abc").asString()); + + // this is equivalent to $dateToString + assertExpression("1970-01-01T00:00:00.123Z", of(Instant.ofEpochMilli(123)).asString()); + + // Arrays and documents are not (yet) supported: + assertThrows(MongoCommandException.class, () -> + assertExpression("[]", ofIntegerArray(1, 2).asString())); + assertThrows(MongoCommandException.class, () -> + assertExpression("[1, 2]", ofIntegerArray(1, 2).asString())); + assertThrows(MongoCommandException.class, () -> + assertExpression("{a: 1}", of(Document.parse("{a: 1}")).asString())); + } + + @Test + public void dateAsStringTest() { + assumeTrue(serverVersionAtLeast(4, 0)); + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/ + final Instant instant = Instant.parse("2007-12-03T10:15:30.005Z"); + MqlDate date = of(instant); + ZonedDateTime utcDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of(ZoneOffset.UTC.getId())); + assertExpression( + "2007-12-03T10:15:30.005Z", + of(instant).asString(), + "{'$toString': [{'$date': '2007-12-03T10:15:30.005Z'}]}"); + // with parameters + assertExpression( + utcDateTime.withZoneSameInstant(ZoneId.of("America/New_York")).format(ISO_LOCAL_DATE_TIME), + date.asString(of("America/New_York"), of("%Y-%m-%dT%H:%M:%S.%L")), + "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " + + "'format': '%Y-%m-%dT%H:%M:%S.%L', " + + "'timezone': 'America/New_York'}}"); + assertExpression( + utcDateTime.withZoneSameInstant(ZoneId.of("+04:30")).format(ISO_LOCAL_DATE_TIME), + date.asString(of("+04:30"), of("%Y-%m-%dT%H:%M:%S.%L")), + "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " + + "'format': '%Y-%m-%dT%H:%M:%S.%L', " + + "'timezone': '+04:30'}}"); + // Olson Timezone Identifier is changed to UTC offset: + assertExpression( + "2007-12-03T05:15:30.005-0500", + of(instant).asString(of("America/New_York"), of("%Y-%m-%dT%H:%M:%S.%L%z"))); + } + + // parse string + + @Test + public void parseDateTest() { + assumeTrue(serverVersionAtLeast(4, 0)); + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/ + String dateString = "2007-12-03T10:15:30.005Z"; + assertExpression( + Instant.parse(dateString), + of(dateString).parseDate(), + "{'$dateFromString': {'dateString': '2007-12-03T10:15:30.005Z'}}"); + + + // throws: "cannot pass in a date/time string with GMT offset together with a timezone argument" + assertThrows(MongoCommandException.class, () -> + assertExpression(1, of("2007-12-03T10:15:30.005+01:00") + .parseDate(of("+01:00"), of("%Y-%m-%dT%H:%M:%S.%L%z")) + .asString())); + // therefore, to parse date strings containing UTC offsets, we need: + assertExpression( + Instant.parse("2007-12-03T09:15:30.005Z"), + of("2007-12-03T10:15:30.005+01:00") + .parseDate(of("%Y-%m-%dT%H:%M:%S.%L%z")), + "{'$dateFromString': {'dateString': '2007-12-03T10:15:30.005+01:00', " + + "'format': '%Y-%m-%dT%H:%M:%S.%L%z'}}"); + + // missing items: + assertExpression( + Instant.parse("2007-12-03T10:15:00.000Z"), + of("2007-12-03T10:15").parseDate(of("%Y-%m-%dT%H:%M"))); + assertThrows(MongoCommandException.class, () -> assertExpression( + "an incomplete date/time string has been found, with elements missing", + of("-12-03T10:15").parseDate(of("-%m-%dT%H:%M")).asString())); + assertThrows(MongoCommandException.class, () -> assertExpression( + "an incomplete date/time string has been found, with elements missing", + of("2007--03T10:15").parseDate(of("%Y--%dT%H:%M")).asString())); + assertThrows(MongoCommandException.class, () -> assertExpression( + "an incomplete date/time string has been found, with elements missing", + of("").parseDate(of("")).asString())); + } + + @Test + public void parseIntegerTest() { + assumeTrue(serverVersionAtLeast(4, 0)); + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLong/ + assertExpression( + 1234, + of("1234").parseInteger(), + "{'$convert': {'input': '1234', 'onError': {'$toLong': '1234'}, 'to': 'int'}}"); + + int intVal = 2_000_000_000; + long longVal = 4_000_000_000L; + assertExpression( + intVal, + of(intVal + "").parseInteger()); + assertExpression( + longVal, + of(longVal + "").parseInteger()); + + // failures + assertThrows(MongoCommandException.class, () -> + assertExpression( + "", + of(BsonDocument.parse("{a:'1.5'}")).getString("a").parseInteger())); + assertThrows(MongoCommandException.class, () -> + assertExpression( + "", + of(BsonDocument.parse("{a:'not an integer'}")).getString("a").parseInteger())); + assertThrows(MongoCommandException.class, () -> + assertExpression( + "", + of("1.5").parseInteger())); + assertThrows(MongoCommandException.class, () -> + assertExpression( + "", + of("not an integer").parseInteger())); + } + + // non-string + + @Test + public void millisecondsToDateTest() { + assumeTrue(serverVersionAtLeast(4, 0)); + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/ + assertExpression( + Instant.ofEpochMilli(1234), + of(1234L).millisecondsAsDate(), + "{'$toDate': {'$numberLong': '1234'}}"); + // This does not accept plain integers: + assertThrows(MongoCommandException.class, () -> + assertExpression( + Instant.parse("2007-12-03T10:15:30.005Z"), + of(1234).millisecondsAsDate(), + "{'$toDate': 1234}")); + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/model/mql/InContextMqlValuesFunctionalTest.java b/driver-sync/src/test/functional/com/mongodb/client/model/mql/InContextMqlValuesFunctionalTest.java new file mode 100644 index 00000000000..e55d74c167a --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/model/mql/InContextMqlValuesFunctionalTest.java @@ -0,0 +1,185 @@ +/* + * 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.mql; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.AggregateIterable; +import com.mongodb.client.DatabaseTestCase; +import com.mongodb.client.FindIterable; +import com.mongodb.client.model.Aggregates; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.client.model.Accumulators.sum; +import static com.mongodb.client.model.Aggregates.match; +import static com.mongodb.client.model.Aggregates.project; +import static com.mongodb.client.model.Filters.expr; +import static com.mongodb.client.model.Projections.computed; +import static com.mongodb.client.model.Projections.excludeId; +import static com.mongodb.client.model.Projections.fields; +import static com.mongodb.client.model.Projections.include; +import static com.mongodb.client.model.Sorts.ascending; +import static com.mongodb.client.model.mql.MqlValues.current; +import static com.mongodb.client.model.mql.MqlValues.of; +import static com.mongodb.client.model.mql.MqlValues.ofArray; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; + +public class InContextMqlValuesFunctionalTest extends DatabaseTestCase { + + private static String bsonToString(final Bson project) { + return project.toBsonDocument(Document.class, MongoClientSettings.getDefaultCodecRegistry()).toString().replaceAll("\"", "'"); + } + + private List aggregate(final Bson... stages) { + AggregateIterable result = collection.aggregate(Arrays.asList(stages)); + List results = new ArrayList<>(); + result.forEach(r -> results.add(r)); + return results; + } + + @Test + public void findTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + collection.insertMany(Arrays.asList( + Document.parse("{_id: 1, x: 0, y: 2}"), + Document.parse("{_id: 2, x: 0, y: 3}"), + Document.parse("{_id: 3, x: 1, y: 3}"))); + + FindIterable iterable = collection.find(expr( + current().getInteger("x").eq(of(1)))); + List results = new ArrayList<>(); + iterable.forEach(r -> results.add(r)); + + assertEquals( + Arrays.asList(Document.parse("{_id: 3, x: 1, y: 3}")), + results); + } + + @Test + public void matchTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + collection.insertMany(Arrays.asList( + Document.parse("{_id: 1, x: 0, y: 2}"), + Document.parse("{_id: 2, x: 0, y: 3}"), + Document.parse("{_id: 3, x: 1, y: 3}"))); + + List results = aggregate( + match(expr(current().getInteger("x").eq(of(1))))); + + assertEquals( + Arrays.asList(Document.parse("{_id: 3, x: 1, y: 3}")), + results); + } + + @Test + public void currentAsMapMatchTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + collection.insertMany(Arrays.asList( + Document.parse("{_id: 1, x: 0, y: 2}"), + Document.parse("{_id: 2, x: 0, y: 3}"), + Document.parse("{_id: 3, x: 1, y: 3}"))); + + List results = aggregate( + match(expr(MqlValues.currentAsMap() + .entries() + .map(e -> e.getValue()) + .sum(v -> v).eq(of(7))))); + + assertEquals( + Arrays.asList(Document.parse("{_id: 3, x: 1, y: 3}")), + results); + } + + @Test + public void projectTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + collection.insertMany(Arrays.asList( + Document.parse("{_id: 1, x: 0, y: 2}"))); + + List expected = Arrays.asList(Document.parse("{_id: 1, x: 0, c: 2}")); + + // old, using "$y" + Bson projectOld = project(fields(include("x"), computed("c", + "$y"))); + assertEquals("{'$project': {'x': 1, 'c': '$y'}}", bsonToString(projectOld)); + assertEquals(expected, + aggregate(projectOld)); + + // new, using current() with add/subtract + Bson projectNew = project(fields(include("x"), computed("c", + current().getInteger("y").add(10).subtract(10)))); + assertEquals( + "{'$project': {'x': 1, 'c': " + + "{'$subtract': [{'$add': [{'$getField': " + + "{'input': '$$CURRENT', 'field': 'y'}}, 10]}, 10]}}}", + bsonToString(projectNew)); + assertEquals(expected, + aggregate(projectNew)); + } + + @Test + public void projectTest2() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + collection.insertMany(Arrays.asList(Document.parse("{_id: 0, x: 1}"))); + + // new, nestedArray + Bson projectNestedArray = project(fields(excludeId(), computed("nestedArray", ofArray( + current().getInteger("x").max(of(4)), + current().getInteger("x"), + of(0), of(1), of(true), of(false) + )))); + assertEquals( + Arrays.asList(Document.parse("{ nestedArray: [ 4, 1, 0, 1, true, false ] }")), + aggregate(projectNestedArray)); + + // new, document + Bson projectDocument = project(fields(computed("nested", + // the below is roughly: "{ x: {$max : ['$x', 4] }}" + of(Document.parse("{x: 9}")).setField("x", current().getInteger("x").max(of(4))) + ))); + assertEquals( + Arrays.asList(Document.parse("{_id: 0, nested: { x: 4 } }")), + aggregate(projectDocument)); + } + + @Test + public void groupTest() { + assumeTrue(serverVersionAtLeast(5, 0)); // get/setField + collection.insertMany(Arrays.asList( + Document.parse("{t: 0, a: 1}"), + Document.parse("{t: 0, a: 2}"), + Document.parse("{t: 1, a: 9}"))); + + List results = aggregate( + Aggregates.group( + current().getInteger("t").add(of(100)), + sum("sum", current().getInteger("a").add(1))), + Aggregates.sort(ascending("_id"))); + assertEquals( + Arrays.asList( + Document.parse("{_id: 100, sum: 5}"), + Document.parse("{_id: 101, sum: 10}")), + results); + } +}