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 super T, MqlBoolean> predicate, final Function super T, ? extends R> 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 super T, ? extends R> 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 super T, ? extends R> 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 super T, ? extends R> 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 super MqlBoolean, ? extends R> 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 super MqlNumber, ? extends R> 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 super MqlInteger, ? extends R> 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 super MqlString, ? extends R> 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 super MqlDate, ? extends R> 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 super MqlArray<@MqlUnchecked(TYPE_ARGUMENT) Q>, ? 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 super MqlDocument, ? extends R> 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 super MqlMap<@MqlUnchecked(TYPE_ARGUMENT) Q>, ? 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 super MqlValue, ? extends R> 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 super T, MqlBoolean> predicate, final Function super T, ? extends R> 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 super T, ? extends R> 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 super T, ? extends R> 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 super T, ? extends R> 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 super MqlBoolean, ? extends R> 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 super MqlNumber, ? extends R> 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 super MqlInteger, ? extends R> 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 super MqlString, ? extends R> 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 super MqlDate, ? extends R> 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 super MqlArray<@MqlUnchecked(TYPE_ARGUMENT) Q>, ? 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 super MqlDocument, ? extends R> 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 super MqlMap<@MqlUnchecked(TYPE_ARGUMENT) Q>, ? 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 super MqlValue, ? extends R> 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 super T, ? extends R> 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 super T, ? extends MqlBoolean> 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 super T, ? extends R> 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 super T, MqlBoolean> predicate);
+
+ /**
+ * Whether all values in {@code this} array satisfy the predicate.
+ *
+ * @param predicate the predicate.
+ * @return the resulting value.
+ */
+ MqlBoolean all(Function super T, MqlBoolean> 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 super T, ? extends MqlNumber> 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 super T, ? extends MqlNumber> 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 super T, MqlString> 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 super T, ? extends MqlArray extends R>> 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 super T, ? extends MqlArray extends R>> 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 super T, ? extends MqlEntry extends R>> 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 extends T> 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 extends T> 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 super MqlArray, ? 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 super MqlBoolean, ? 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 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 super MqlDate, ? 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 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 extends T> 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 super MqlDocument, ? 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 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 extends R> 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 extends R> 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 super MqlValue, ? extends R> 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 super MqlBoolean, ? extends R> 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 super MqlInteger, ? extends R> 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 super MqlNumber, ? extends R> 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 super MqlString, ? extends R> 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 super MqlDate, ? extends R> 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 super MqlArray, ? 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 super MqlMap, ? 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 super MqlDocument, ? extends R> 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 extends R> 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