diff --git a/source/agg-exp-ops.txt b/source/agg-exp-ops.txt new file mode 100644 index 00000000..ed87ef0d --- /dev/null +++ b/source/agg-exp-ops.txt @@ -0,0 +1,1180 @@ +.. _kotlin-sync-aggregation-expression-operations: + +================================= +Aggregation Expression Operations +================================= + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +.. facet:: + :name: genre + :values: reference + +.. meta:: + :keywords: transform data, fluent interface + +Overview +-------- + +In this guide, you can learn how to use the {+driver-short+} to construct +expressions for use in aggregation pipelines. You can perform +expression operations with discoverable, typesafe {+language+} methods rather +than by using BSON documents. Because these methods follow the fluent interface +pattern, you can chain aggregation operations together to create code +that is compact and naturally readable. + +The operations in this guide use methods from the +`com.mongodb.client.model.mql <{+core-api+}/com/mongodb/client/model/mql/package-summary.html>`__ package. +These methods provide an idiomatic way to use the Query API, +the mechanism by which the driver interacts with a MongoDB deployment. To learn more +about the Query API, see the :manual:`Server manual documentation `. + +How to Use Operations +--------------------- + +The examples in this guide assume that you include the following imports +in your code: + +.. code-block:: kotlin + :copyable: true + + import com.mongodb.client.model.Aggregates + import com.mongodb.client.model.Accumulators + import com.mongodb.client.model.Projections + import com.mongodb.client.model.Filters + import com.mongodb.client.model.mql.MqlValues + +To access document fields in an expression, you must reference the +current document being processed by the aggregation pipeline by using the +``current()`` method. To access the value of a +field, you must use the appropriately typed method, such as +``getString()`` or ``getDate()``. When you specify the type for a field, +you ensure that the driver provides only those methods which are +compatible with that type. The following code shows how to reference a +string field called ``name``: + +.. code-block:: kotlin + :copyable: true + + current().getString("name") + +To specify a value in an operation, pass it to the ``of()`` constructor method to +convert it to a valid type. The following code shows how to reference a +value of ``1.0``: + +.. code-block:: kotlin + :copyable: true + + of(1.0) + +To create an operation, chain a method to your field or value reference. +You can build more complex operations by chaining multiple methods. + +The following example creates an operation to find patients in New +Mexico who have visited the doctor’s office at least once. The operation +performs the following actions: + +- Checks if the size of the ``visitDates`` array value is greater than ``0`` + by using the ``gt()`` method +- Checks if the ``state`` field value is “New Mexico” by using the + ``eq()`` method + +The ``and()`` method links these operations so that the pipeline stage +matches only documents that meet both criteria. + +.. code-block:: kotlin + :copyable: true + + current() + .getArray("visitDates") + .size() + .gt(of(0)) + .and(current() + .getString("state") + .eq(of("New Mexico"))) + +While some aggregation stages, such as ``group()``, accept operations +directly, other stages expect that you first include your operation in a +method such as ``computed()`` or ``expr()``. These methods, which take +values of type ``TExpression``, allow you to use your expressions in +certain aggregations. + +To complete your aggregation pipeline stage, include your expression +in an aggregates builder method. The following list provides examples of +how to include your expression in common aggregates builder methods: + +- ``match(expr())`` +- ``project(fields(computed("", )))`` +- ``group()`` + +.. TODO To learn more about these methods, see the +.. :ref:`kotlin-sync-aggregation`. + +Constructor Methods +------------------- + +You can use these constructor methods to define values for use in {+language+} aggregation +expressions. + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Method + - Description + + * - `current() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#current()>`__ + - References the current document being processed by the aggregation pipeline. + + * - `currentAsMap() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#currentAsMap()>`__ + - References the current document being processed by the aggregation pipeline as a map value. + + * - | `of() for MqlBoolean <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#of(boolean)>`__ + | `of() for MqlNumber (double) <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#of(double)>`__ + | `of() for MqlNumber (Decimal128) <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#of(org.bson.types.Decimal128)>`__ + | `of() for MqlInteger (int) <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#of(int)>`__ + | `of() for MqlInteger (long) <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#of(long)>`__ + | `of() for MqlString <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#of(java.lang.String)>`__ + | `of() for MqlDate <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#of(java.time.Instant)>`__ + | `of() for MqlDocument <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#of(org.bson.conversions.Bson)>`__ + + - Returns an ``MqlValue`` type corresponding to the provided primitive. + + * - `ofArray() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#ofArray(T...)>`__ + + | **Typed Variants**: + | `ofBooleanArray() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#ofBooleanArray(boolean...)>`__ + | `ofDateArray() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#ofDateArray(java.time.Instant...)>`__ + | `ofIntegerArray() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#ofIntegerArray(int...)>`__ + | `ofNumberArray() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#ofNumberArray(double...)>`__ + | `ofStringArray() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#ofStringArray(java.lang.String...)>`__ + + - Returns an array of ``MqlValue`` types corresponding to the provided array of primitives. + + * - `ofEntry() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#ofEntry(com.mongodb.client.model.mql.MqlString,T)>`__ + - Returns an entry value. + + * - `ofMap() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#ofMap()>`__ + - Returns an empty map value. + + * - `ofNull() <{+core-api+}/com/mongodb/client/model/mql/MqlValues.html#ofNull()>`__ + - Returns the null value as exists in the Query API. + +.. important:: + + When you provide a value to one of these methods, the driver treats + it literally. For example, ``of("$x")`` represents the string value + ``"$x"``, rather than a field named ``x``. + +Refer to any of the sections under :ref:`Operations +` for examples using these +methods. + +.. _kotlin-sync-aggregation-expression-ops-section: + +Operations +---------- + +The following sections provide information and examples for +aggregation expression operations available in the driver. +The operations are categorized by purpose and functionality. + +Each section has a table that describes aggregation methods +available in the driver and corresponding expression operators in the +Query API. The method names link to API documentation and the +aggregation pipeline operator names link to descriptions and examples in +the Server manual documentation. While each method is effectively +equivalent to the corresponding aggregation operator, they may differ in +expected parameters and implementation. + +The example in each section uses the ``listOf()`` method to create a +pipeline from the aggregation stage. Then, each example passes the +pipeline to the ``aggregate()`` method of ``MongoCollection``. + +.. note:: + + The driver generates a Query API expression that may be different + from the Query API expression provided in each example. However, + both expressions will produce the same aggregation result. + +.. important:: + + The driver does not provide methods for all aggregation pipeline operators in + the Query API. To use an unsupported operation in an + aggregation, you must define the entire expression using the BSON ``Document`` + type. + +.. TODO add to note To learn more about the ``Document`` type, see :ref:``. + +Arithmetic Operations +~~~~~~~~~~~~~~~~~~~~~ + +You can perform an arithmetic operation on a value of type ``MqlInteger`` or +``MqlNumber`` using the methods described in this section. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - | `abs() for MqlInteger <{+core-api+}/com/mongodb/client/model/mql/MqlInteger.html#abs()>`__ + | `abs() for MqlNumber <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#abs()>`__ + + - :manual:`$abs ` + + * - | `add() for MqlInteger <{+core-api+}/com/mongodb/client/model/mql/MqlInteger.html#add(int)>`__ + | `add() for MqlNumber <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#add(com.mongodb.client.model.mql.MqlNumber)>`__ + + - :manual:`$add ` + + * - `divide() <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#divide(com.mongodb.client.model.mql.MqlNumber)>`__ + - :manual:`$divide ` + + * - | `multiply() for MqlInteger <{+core-api+}/com/mongodb/client/model/mql/MqlInteger.html#multiply(int)>`__ + | `multiply() for MqlNumber <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#multiply(com.mongodb.client.model.mql.MqlNumber)>`__ + + - :manual:`$multiply ` + + * - `round() <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#round()>`__ + - :manual:`$round ` + + * - | `subtract() for MqlInteger <{+core-api+}/com/mongodb/client/model/mql/MqlInteger.html#subtract(int)>`__ + | `subtract() for MqlNumber <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#subtract(com.mongodb.client.model.mql.MqlNumber)>`__ + + - :manual:`$subtract ` + +Suppose you have weather data for a specific year that includes the +precipitation measurement (in inches) for each day. You want to find the +average precipitation, in millimeters, for each month. + +The ``multiply()`` operator multiplies the ``precipitation`` field by +``25.4`` to convert the field value to millimeters. The ``avg()`` accumulator method +returns the average as the ``avgPrecipMM`` field. The ``group()`` method +groups the values by month given in each document's ``date`` field. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-arithmetic-aggregation + :end-before: end-arithmetic-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in the +Query API: + +.. code-block:: javascript + :copyable: true + + [ { $group: { + _id: { $month: "$date" }, + avgPrecipMM: { + $avg: { $multiply: ["$precipitation", 25.4] } } + } } ] + +Array Operations +~~~~~~~~~~~~~~~~ + +You can perform an array operation on a value of type ``MqlArray`` +using the methods described in this section. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `all() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#all(java.util.function.Function)>`__ + - :manual:`$allElementsTrue ` + + * - `any() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#any(java.util.function.Function)>`__ + - :manual:`$anyElementTrue ` + + * - `concat() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#concat(com.mongodb.client.model.mql.MqlArray)>`__ + - :manual:`$concatArrays ` + + * - `concatArrays() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#concatArrays(java.util.function.Function)>`__ + - :manual:`$concatArrays ` + + * - `contains() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#contains(T)>`__ + - :manual:`$in ` + + * - `distinct() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#distinct()>`__ + - :manual:`$setUnion ` + + * - `elementAt() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#elementAt(int)>`__ + - :manual:`$arrayElemAt ` + + * - `filter() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#filter(java.util.function.Function)>`__ + - :manual:`$filter ` + + * - `first() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#first()>`__ + - :manual:`$first ` + + * - `joinStrings() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#joinStrings(java.util.function.Function)>`__ + - :manual:`$concat ` + + * - `last() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#last()>`__ + - :manual:`$last ` + + * - `map() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#map(java.util.function.Function)>`__ + - :manual:`$map ` + + * - `max() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#max(T)>`__ + - :manual:`$max ` + + * - `maxN() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#maxN(com.mongodb.client.model.mql.MqlInteger)>`__ + - :manual:`$maxN ` + + * - `min() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#min(T)>`__ + - :manual:`$min ` + + * - `minN() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#minN(com.mongodb.client.model.mql.MqlInteger)>`__ + - :manual:`$minN ` + + * - `multiply() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#multiply(java.util.function.Function)>`__ + - :manual:`$multiply ` + + * - `size() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#size()>`__ + - :manual:`$size ` + + * - `slice() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#slice(int,int)>`__ + - :manual:`$slice ` + + * - `sum() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#sum(java.util.function.Function)>`__ + - :manual:`$sum ` + + * - `union() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#union(com.mongodb.client.model.mql.MqlArray)>`__ + - :manual:`$setUnion ` + + * - `unionArrays() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#unionArrays(java.util.function.Function)>`__ + - :manual:`$setUnion ` + +Suppose you have a collection of movies, each of which contains an array +of nested documents for upcoming showtimes. Each nested document +contains an array that represents the total number of seats in the +theater, where the first array entry is the number of premium seats and +the second entry is the number of regular seats. Each nested document +also contains the number of tickets that have already been bought for +the showtime. A document in this collection might resemble the +following: + +.. code-block:: json + :copyable: false + + { + "_id": ..., + "movie": "Hamlet", + "showtimes": [ + { + "date": "May 14, 2023, 12:00 PM", + "seats": [ 20, 80 ], + "ticketsBought": 100 + }, + { + "date": "May 20, 2023, 08:00 PM", + "seats": [ 10, 40 ], + "ticketsBought": 34 + }] + } + +The ``filter()`` method displays only the results matching the provided +predicate. In this case, the predicate uses ``sum()`` to calculate the +total number of seats and compares that value to the number of ``ticketsBought`` +by using the ``lt()`` method. The ``project()`` method stores these +filtered results as a new ``availableShowtimes`` array field. + +.. tip:: + + You must specify the type of values that an array contains when using + the ``getArray()`` method to work with the values as any specific + type. For example, you must specify that an array contains integers + to perform calculations with those integers elsewhere in + your application. + + The example in this section specifies that the ``seats`` array + contains values of type ``MqlDocument`` so that it can extract nested + fields from each array entry. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-array-aggregation + :end-before: end-array-aggregation + :language: kotlin + :copyable: + :dedent: + +.. note:: + + To improve readability, the previous example assigns intermediary values to + the ``totalSeats`` and ``isAvailable`` variables. If you don't assign + these intermediary values to variables, the code still produces + equivalent results. + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $project: { + availableShowtimes: { + $filter: { + input: "$showtimes", + as: "showtime", + cond: { $lt: [ "$$showtime.ticketsBought", { $sum: "$$showtime.seats" } ] } + } } + } } ] + +Boolean Operations +~~~~~~~~~~~~~~~~~~ + +You can perform a boolean operation on a value of type ``MqlBoolean`` +using the methods described in this section. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `and() <{+core-api+}/com/mongodb/client/model/mql/MqlBoolean.html#and(com.mongodb.client.model.mql.MqlBoolean)>`__ + - :manual:`$and ` + + * - `not() <{+core-api+}/com/mongodb/client/model/mql/MqlBoolean.html#not()>`__ + - :manual:`$not ` + + * - `or() <{+core-api+}/com/mongodb/client/model/mql/MqlBoolean.html#or(com.mongodb.client.model.mql.MqlBoolean)>`__ + - :manual:`$or ` + +Suppose you want to classify very low or high weather temperature +readings (in degrees Fahrenheit) as extreme. + +The ``or()`` operator checks to see if temperatures are extreme by comparing +the ``temperature`` field to predefined values by using the ``lt()`` and +``gt()`` methods. The ``project()`` method records this result in the +``extremeTemp`` field. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-boolean-aggregation + :end-before: end-boolean-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $project: { + extremeTemp: { $or: [ { $lt: ["$temperature", 10] }, + { $gt: ["$temperature", 95] } ] } + } } ] + +Comparison Operations +~~~~~~~~~~~~~~~~~~~~~ + +You can perform a comparison operation on a value of type ``MqlValue`` +using the methods described in this section. + +.. tip:: + + The ``cond()`` method is similar to the ternary operator in {+language+} and you + can use it for simple branches based on boolean values. Use + the ``switchOn()`` methods for more complex comparisons such as performing + pattern matching on the value type or other arbitrary checks on the value. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `eq() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#eq(com.mongodb.client.model.mql.MqlValue)>`__ + - :manual:`$eq ` + + * - `gt() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#gt(com.mongodb.client.model.mql.MqlValue)>`__ + - :manual:`$gt ` + + * - `gte() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#gte(com.mongodb.client.model.mql.MqlValue)>`__ + - :manual:`$gte ` + + * - `lt() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#lt(com.mongodb.client.model.mql.MqlValue)>`__ + - :manual:`$lt ` + + * - `lte() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#lte(com.mongodb.client.model.mql.MqlValue)>`__ + - :manual:`$lte ` + + * - | `max() for MqlInteger <{+core-api+}/com/mongodb/client/model/mql/MqlInteger.html#max(com.mongodb.client.model.mql.MqlInteger)>`__ + | `max() for MqlNumber <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#max(com.mongodb.client.model.mql.MqlNumber)>`__ + + - :manual:`$max ` + + * - | `min() for MqlInteger <{+core-api+}/com/mongodb/client/model/mql/MqlInteger.html#min(com.mongodb.client.model.mql.MqlInteger)>`__ + | `min() for MqlNumber <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#min(com.mongodb.client.model.mql.MqlNumber)>`__ + + - :manual:`$min ` + + * - `ne() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#ne(com.mongodb.client.model.mql.MqlValue)>`__ + - :manual:`$ne ` + +The following example shows a pipeline that matches all the documents +where the ``location`` field has the value ``"California"``: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-comparison-aggregation + :end-before: end-comparison-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $match: { location: { $eq: "California" } } } ] + +Conditional Operations +~~~~~~~~~~~~~~~~~~~~~~ + +You can perform a conditional operation using the methods described in +this section. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `cond() <{+core-api+}/com/mongodb/client/model/mql/MqlBoolean.html#cond(T,T)>`__ + - :manual:`$cond ` + + * - `switchOn() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#switchOn(java.util.function.Function)>`__ + + | **Typed Variants**: + | `switchArrayOn() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#switchArrayOn(java.util.function.Function)>`__ + | `switchBooleanOn() <{+core-api+}/com/mongodb/client/model/mql/MqlBoolean.html#switchBooleanOn(java.util.function.Function)>`__ + | `switchDateOn() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#switchDateOn(java.util.function.Function)>`__ + | `switchDocumentOn() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#switchDocumentOn(java.util.function.Function)>`__ + | `switchIntegerOn() <{+core-api+}/com/mongodb/client/model/mql/MqlInteger.html#switchIntegerOn(java.util.function.Function)>`__ + | `switchMapOn() <{+core-api+}/com/mongodb/client/model/mql/MqlMap.html#switchMapOn(java.util.function.Function)>`__ + | `switchNumberOn() <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#switchNumberOn(java.util.function.Function)>`__ + | `switchStringOn() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#switchStringOn(java.util.function.Function)>`__ + + - :manual:`$switch ` + +Suppose you have a collection of customers with their membership information. +Originally, customers were either members or not. Over time, membership levels +were introduced and used the same field. The information stored in this field +can be one of a few different types, and you want to create a standardized value +indicating their membership level. + +The ``switchOn()`` method checks each clause in order. If the value matches the +type indicated by the clause, then the clause determines the string value +corresponding to the membership level. If the original value is a string, it +represents the membership level and that value is used. If the data type is a +boolean, it returns either ``Gold`` or ``Guest`` for the membership level. If +the data type is an array, it returns the most recent string in the array which +matches the most recent membership level. If the ``member`` field is an +unknown type, the ``switchOn()`` method provides a default value of ``Guest``. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-conditional-aggregation + :end-before: end-conditional-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $project: { + membershipLevel: { + $switch: { + branches: [ + { case: { $eq: [ { $type: "$member" }, "string" ] }, then: "$member" }, + { case: { $eq: [ { $type: "$member" }, "bool" ] }, then: { $cond: { + if: "$member", + then: "Gold", + else: "Guest" } } }, + { case: { $eq: [ { $type: "$member" }, "array" ] }, then: { $last: "$member" } } + ], + default: "Guest" } } + } } ] + +Convenience Operations +~~~~~~~~~~~~~~~~~~~~~~ + +You can apply custom functions to values of type +``MqlValue`` using the methods described in this section. + +To improve readability and allow for code reuse, you can move redundant +code into static methods. However, you cannot directly chain +static methods in {+language+}. The ``passTo()`` method lets you chain values +into custom static methods. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `passTo() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#passTo(java.util.function.Function)>`__ + + | **Typed Variants**: + | `passArrayTo() <{+core-api+}/com/mongodb/client/model/mql/MqlArray.html#passArrayTo(java.util.function.Function)>`__ + | `passBooleanTo() <{+core-api+}/com/mongodb/client/model/mql/MqlBoolean.html#passBooleanTo(java.util.function.Function)>`__ + | `passDateTo() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#passDateTo(java.util.function.Function)>`__ + | `passDocumentTo() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#passDocumentTo(java.util.function.Function)>`__ + | `passIntegerTo() <{+core-api+}/com/mongodb/client/model/mql/MqlInteger.html#passIntegerTo(java.util.function.Function)>`__ + | `passMapTo() <{+core-api+}/com/mongodb/client/model/mql/MqlMap.html#passMapTo(java.util.function.Function)>`__ + | `passNumberTo() <{+core-api+}/com/mongodb/client/model/mql/MqlNumber.html#passNumberTo(java.util.function.Function)>`__ + | `passStringTo() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#passStringTo(java.util.function.Function)>`__ + + - *No corresponding operator* + +Suppose you want to determine how a class is performing against some +benchmarks. You want to find the average final grade for each class and +compare it against the benchmark values. + +The following custom method ``gradeAverage()`` takes an array of documents and +the name of an integer field shared across those documents. It calculates the +average of that field across all the documents in the provided array and +determines the average of that field across all the elements in +the provided array. The ``evaluate()`` method compares a provided value to +two provided range limits and generates a response string based on +how the values compare: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-convenience-aggregation-methods + :end-before: end-convenience-aggregation-methods + :language: kotlin + :copyable: + :dedent: + +.. tip:: + + Using the ``passTo()`` method allows you to reuse + your custom methods for other aggregations. For example, you can + use the ``gradeAverage()`` method to find the average of grades for + groups of students filtered by entry year or district, not just their + class. Similarly, you could use the ``evaluate()`` method to evaluate + an individual student's performance or an entire school's performance. + +The ``passArrayTo()`` method takes an array of all students and calculates the +average score by using the ``gradeAverage()`` method. Then, the +``passNumberTo()`` method uses the ``evaluate()`` method to determine how the +classes are performing. This example stores the result as the ``evaluation`` +field using the ``project()`` method. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-convenience-aggregation + :end-before: end-convenience-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $project: { + evaluation: { $switch: { + branches: [ + { case: { $lte: [ { $avg: "$students.finalGrade" }, 70 ] }, + then: "Needs improvement" + }, + { case: { $lte: [ { $avg: "$students.finalGrade" }, 85 ] }, + then: "Meets expectations" + } + ], + default: "Exceeds expectations" } } + } } ] + +Conversion Operations +~~~~~~~~~~~~~~~~~~~~~ + +You can perform a conversion operation to convert between certain ``MqlValue`` +types using the methods described in this section. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `asDocument() <{+core-api+}/com/mongodb/client/model/mql/MqlMap.html#asDocument()>`__ + - *No corresponding operator* + + * - `asMap() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#asMap()>`__ + - *No corresponding operator* + + * - `asString() for MqlDate <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#asString(com.mongodb.client.model.mql.MqlString,com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$dateToString ` + + * - `asString() for MqlValue <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#asString()>`__ + - :manual:`$toString ` + + * - `millisecondsAsDate() <{+core-api+}/com/mongodb/client/model/mql/MqlInteger.html#millisecondsAsDate()>`__ + - :manual:`$toDate ` + + * - `parseDate() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#parseDate()>`__ + - :manual:`$dateFromString ` + + * - `parseInteger() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#parseInteger()>`__ + - :manual:`$toInt ` + +Suppose you want to have a collection of student data that includes +their graduation years, which are stored as strings. You want to +calculate the year of their five-year reunion and store this value in a +new field. + +The ``parseInteger()`` method converts the ``graduationYear`` to an integer +so that ``add()`` can calculate the reunion year. The ``addFields()`` method +stores this result as a new ``reunionYear`` field. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-convenience-aggregation + :end-before: end-convenience-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $addFields: { + reunionYear: { + $add: [ { $toInt: "$graduationYear" }, 5 ] } + } } ] + +Date Operations +~~~~~~~~~~~~~~~ + +You can perform a date operation on a value of type ``MqlDate`` +using the methods described in this section. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `dayOfMonth() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#dayOfMonth(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$dayOfMonth ` + + * - `dayOfWeek() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#dayOfWeek(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$dayOfWeek ` + + * - `dayOfYear() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#dayOfYear(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$dayOfYear ` + + * - `hour() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#hour(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$hour ` + + * - `millisecond() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#millisecond(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$millisecond ` + + * - `minute() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#minute(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$minute ` + + * - `month() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#month(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$month ` + + * - `second() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#second(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$second ` + + * - `week() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#week(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$week ` + + * - `year() <{+core-api+}/com/mongodb/client/model/mql/MqlDate.html#year(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$year ` + +Suppose you have data about package deliveries and want to match +deliveries that occurred on any Monday in the ``"America/New_York"`` time +zone. + +If the ``deliveryDate`` field contains any string values representing +valid dates, such as ``"2018-01-15T16:00:00Z"`` or ``"Jan 15, 2018, 12:00 +PM EST"``, you can use the ``parseDate()`` method to convert the strings +into date types. + +The ``dayOfWeek()`` method determines which day of the week that a date +is, then converts it to a number. The number assignment uses ``0`` to mean +Sunday when using the ``"America/New_York"`` timezone. The ``eq()`` +method compares this value to ``2``, or Monday. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-date-aggregation + :end-before: end-date-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $match: { + $expr: { + $eq: [ { + $dayOfWeek: { + date: { $dateFromString: { dateString: "$deliveryDate" } }, + timezone: "America/New_York" }}, + 2 + ] } + } } ] + +Document Operations +~~~~~~~~~~~~~~~~~~~ + +You can perform a document operation on a value of type ``MqlDocument`` +using the methods described in this section. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - | `getArray() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#getArray(java.lang.String)>`__ + | `getBoolean() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#getBoolean(java.lang.String)>`__ + | `getDate() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#getDate(java.lang.String)>`__ + | `getDocument() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#getDocument(java.lang.String)>`__ + | `getField() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#getField(java.lang.String)>`__ + | `getInteger() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#getInteger(java.lang.String)>`__ + | `getMap() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#getMap(java.lang.String)>`__ + | `getNumber() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#getNumber(java.lang.String)>`__ + | `getString() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#getString(java.lang.String)>`__ + - :manual:`$getField ` + + * - `hasField() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#hasField(java.lang.String)>`__ + - *No corresponding operator* + + * - `merge() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#merge(com.mongodb.client.model.mql.MqlDocument)>`__ + - :manual:`$mergeObjects ` + + * - `setField() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#setField(java.lang.String,com.mongodb.client.model.mql.MqlValue)>`__ + - :manual:`$setField ` + + * - `unsetField() <{+core-api+}/com/mongodb/client/model/mql/MqlDocument.html#unsetField(java.lang.String)>`__ + - :manual:`$unsetField ` + +Suppose you have a collection of legacy customer data which includes +addresses as child documents under the ``mailing.address`` field. You want +to find all the customers who live in Washington state. A +document in this collection might resemble the following: + +.. code-block:: json + :copyable: false + + { + "_id": ..., + "customer.name": "Mary Kenneth Keller", + "mailing.address": + { + "street": "601 Mongo Drive", + "city": "Vasqueztown", + "state": "CO", + "zip": 27017 + } + } + +The ``getDocument()`` method retrieves the ``mailing.address`` field as a +document so the nested ``state`` field can be retrieved with the +``getString()`` method. The ``eq()`` method checks if the value of the +``state`` field is ``"WA"``. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-document-aggregation + :end-before: end-document-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ + { $match: { + $expr: { + $eq: [{ + $getField: { + input: { $getField: { input: "$$CURRENT", field: "mailing.address"}}, + field: "state" }}, + "WA" ] + }}}] + +Map Operations +~~~~~~~~~~~~~~ + +You can perform a map operation on a value of either type ``MqlMap`` or +``MqlEntry`` using the methods described in this section. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `entries() <{+core-api+}/com/mongodb/client/model/mql/MqlMap.html#entries()>`__ + - :manual:`$objectToArray ` + + * - `get() <{+core-api+}/com/mongodb/client/model/mql/MqlMap.html#get(com.mongodb.client.model.mql.MqlString)>`__ + - *No corresponding operator* + + * - `getKey() <{+core-api+}/com/mongodb/client/model/mql/MqlEntry.html#getKey()>`__ + - *No corresponding operator* + + * - `getValue() <{+core-api+}/com/mongodb/client/model/mql/MqlEntry.html#getValue()>`__ + - *No corresponding operator* + + * - `has() <{+core-api+}/com/mongodb/client/model/mql/MqlMap.html#has(com.mongodb.client.model.mql.MqlString)>`__ + - *No corresponding operator* + + * - `merge() <{+core-api+}/com/mongodb/client/model/mql/MqlMap.html#merge(com.mongodb.client.model.mql.MqlMap)>`__ + - *No corresponding operator* + + * - `set() <{+core-api+}/com/mongodb/client/model/mql/MqlMap.html#set(com.mongodb.client.model.mql.MqlString,T)>`__ + - *No corresponding operator* + + * - `setKey() <{+core-api+}/com/mongodb/client/model/mql/MqlEntry.html#setKey(com.mongodb.client.model.mql.MqlString)>`__ + - *No corresponding operator* + + * - `setValue() <{+core-api+}/com/mongodb/client/model/mql/MqlEntry.html#setValue(T)>`__ + - *No corresponding operator* + + * - `unset() <{+core-api+}/com/mongodb/client/model/mql/MqlMap.html#unset(com.mongodb.client.model.mql.MqlString)>`__ + - *No corresponding operator* + +Suppose you have a collection of inventory data where each document represents +an individual item you're responsible for supplying. Each document contains a +field that is a map of all your warehouses and how many copies they +have in their inventory of the item. You want to determine the total number of +copies of items you have across all warehouses. A document in this +collection might resemble the following: + +.. code-block:: json + :copyable: false + + { + "_id": ..., + "item": "notebook" + "warehouses": [ + { "Atlanta", 50 }, + { "Chicago", 0 }, + { "Portland", 120 }, + { "Dallas", 6 } + ] + } + +The ``entries()`` method returns the map entries in the ``warehouses`` +field as an array. The ``sum()`` method calculates the total value of items +based on the values in the array retrieved with the ``getValue()`` method. +This example stores the result as the new ``totalInventory`` field using the +``project()`` method. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-map-aggregation + :end-before: end-map-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $project: { + totalInventory: { + $sum: { + $getField: { $objectToArray: "$warehouses" }, + } } + } } ] + +String Operations +~~~~~~~~~~~~~~~~~ + +You can perform a string operation on a value of type ``MqlString`` +using the methods described in this section. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `append() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#append(com.mongodb.client.model.mql.MqlString)>`__ + - :manual:`$concat ` + + * - `length() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#length()>`__ + - :manual:`$strLenCP ` + + * - `lengthBytes() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#lengthBytes()>`__ + - :manual:`$strLenBytes ` + + * - `substr() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#substr(int,int)>`__ + - :manual:`$substrCP ` + + * - `substrBytes() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#substrBytes(int,int)>`__ + - :manual:`$substrBytes ` + + * - `toLower() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#toLower()>`__ + - :manual:`$toLower ` + + * - `toUpper() <{+core-api+}/com/mongodb/client/model/mql/MqlString.html#toUpper()>`__ + - :manual:`$toUpper ` + +Suppose you want to generate lowercase usernames for employees of a +company from the employees' last names and employee IDs. + +The ``append()`` method combines the ``lastName`` and ``employeeID`` fields into +a single username, while the ``toLower()`` method makes the entire username +lowercase. This example stores the result as a new ``username`` field using +the ``project()`` method. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-string-aggregation + :end-before: end-string-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $project: { + username: { + $toLower: { $concat: ["$lastName", "$employeeID"] } } + } } ] + +Type-Checking Operations +~~~~~~~~~~~~~~~~~~~~~~~~ + +You can perform a type-check operation on a value of type ``MqlValue`` +using the methods described in this section. + +These methods do not return boolean values. Instead, you provide a default value +that matches the type specified by the method. If the checked value +matches the method type, the checked value is returned. Otherwise, the supplied +default value is returned. To program branching logic based on the +data type, see ``switchOn()``. + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Method + - Aggregation Pipeline Operator + + * - `isArrayOr() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#isArrayOr(com.mongodb.client.model.mql.MqlArray)>`__ + - *No corresponding operator* + + * - `isBooleanOr() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#isBooleanOr(com.mongodb.client.model.mql.MqlBoolean)>`__ + - *No corresponding operator* + + * - `isDateOr() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#isDateOr(com.mongodb.client.model.mql.MqlDate)>`__ + - *No corresponding operator* + + * - `isDocumentOr() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#isDocumentOr(T)>`__ + - *No corresponding operator* + + * - `isIntegerOr() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#isIntegerOr(com.mongodb.client.model.mql.MqlInteger)>`__ + - *No corresponding operator* + + * - `isMapOr() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#isMapOr(com.mongodb.client.model.mql.MqlMap)>`__ + - *No corresponding operator* + + * - `isNumberOr() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#isNumberOr(com.mongodb.client.model.mql.MqlNumber)>`__ + - *No corresponding operator* + + * - `isStringOr() <{+core-api+}/com/mongodb/client/model/mql/MqlValue.html#isStringOr(com.mongodb.client.model.mql.MqlString)>`__ + - *No corresponding operator* + +Suppose you have a collection of rating data. An early version of the review +schema allowed users to submit negative reviews without a star rating. You want +convert any of these negative reviews without a star rating to have the minimum +value of 1 star. + +The ``isNumberOr()`` method returns either the value of ``rating``, or +a value of ``1`` if ``rating`` is not a number or is null. The +``project()`` method returns this value as a new ``numericalRating`` field. + +The following code shows the pipeline for this aggregation: + +.. literalinclude:: /includes/aggregation/aggExpressions.kt + :start-after: start-type-aggregation + :end-before: end-type-aggregation + :language: kotlin + :copyable: + :dedent: + +The following code provides an equivalent aggregation pipeline in +the Query API: + +.. code-block:: javascript + :copyable: true + + [ { $project: { + numericalRating: { + $cond: { if: { $isNumber: "$rating" }, + then: "$rating", + else: 1 + } } + } } ] diff --git a/source/includes/aggregation/aggExpressions.kt b/source/includes/aggregation/aggExpressions.kt new file mode 100644 index 00000000..b58a2b36 --- /dev/null +++ b/source/includes/aggregation/aggExpressions.kt @@ -0,0 +1,529 @@ +import com.mongodb.client.model.* +import com.mongodb.client.model.mql.* +import com.mongodb.client.model.mql.MqlValues.current +import com.mongodb.client.model.mql.MqlValues.of +import com.mongodb.kotlin.client.MongoClient +import org.bson.Document +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class AggExpressionsTest { + companion object { + val uri = "" + val mongoClient = MongoClient.create(uri) + private val database = mongoClient.getDatabase("aggExpressions") + + @AfterAll + @JvmStatic + fun afterAll() { + mongoClient.close() + } + } + + @Test + fun arithmeticOpsTest() { + + data class Entry(val date: LocalDate, val precipitation: Double) + + val collection = database.getCollection("weatherData") + + val entries = listOf( + Entry(LocalDate.of(2021, 6, 24), 5.0), + Entry(LocalDate.of(2021, 6, 25), 1.4) + ) + + collection.insertMany(entries) + + // start-arithmetic-aggregation + val month = current().getDate("date").month(of("UTC")) + val precip = current().getInteger("precipitation") + + val results = collection.aggregate( + listOf( + Aggregates.group( + month, + Accumulators.avg("avgPrecipMM", precip.multiply(25.4)) + ) + ) + ) + // end-arithmetic-aggregation + + val result = results.toList() + assertEquals(1, result.size) + assertEquals(6, result[0].getInteger("_id")) + assertEquals(81.28, result[0].getDouble("avgPrecipMM")) + + collection.drop() + } + + @Test + fun arrayOpsTest() { + + data class Showtime( + val date: String, + val seats: List, + val ticketsBought: Int + ) + + data class Movie( + val movie: String, + val showtimes: List, + ) + + val collection = database.getCollection("movies") + + val entries = listOf( + Movie( + "Hamlet", + listOf( + Showtime("May 14, 2023, 12:00 PM", listOf(20, 80), 100), + Showtime("May 20, 2023, 08:00 PM", listOf(10, 40), 34) + ) + ) + ) + + collection.insertMany(entries) + + // start-array-aggregation + val showtimes = current().getArray("showtimes") + + val results = collection.aggregate( + listOf( + Aggregates.project( + Projections.fields( + Projections.computed("availableShowtimes", showtimes + .filter { showtime -> + val seats = showtime.getArray("seats") + val totalSeats = seats.sum { n -> n } + val ticketsBought = showtime.getInteger("ticketsBought") + val isAvailable = ticketsBought.lt(totalSeats) + isAvailable + }) + ) + ) + ) + ) + // end-array-aggregation + + val result = results.toList() + assertEquals(1, result.size) + + collection.drop() + } + + @Test + fun booleanOpsTest() { + + data class Entry(val location: String, val temperature: Int) + + val collection = database.getCollection("tempData") + + val entries = listOf( + Entry("Randolph, NJ", 100), + Entry("Seward, AK", 1), + Entry("Lincoln, NE", 45) + ) + + collection.insertMany(entries) + + // start-boolean-aggregation + val temperature = current().getInteger("temperature") + + val results = collection.aggregate( + listOf( + Aggregates.project( + Projections.fields( + Projections.computed( + "extremeTemp", temperature + .lt(of(10)) + .or(temperature.gt(of(95))) + ) + ) + ) + ) + ) + // end-boolean-aggregation + + val result = results.toList() + assertEquals(3, result.size) + assertEquals(true, result[0].getBoolean("extremeTemp")) + assertEquals(true, result[1].getBoolean("extremeTemp")) + assertEquals(false, result[2].getBoolean("extremeTemp")) + + collection.drop() + } + + @Test + fun comparisonOpsTest() { + + data class Place(val location: String) + + val collection = database.getCollection("placesData") + + val entries = listOf( + Place("Delaware"), + Place("California") + ) + + collection.insertMany(entries) + + // start-comparison-aggregation + val location = current().getString("location") + + val results = collection.aggregate( + listOf( + Aggregates.match( + Filters.expr(location.eq(of("California"))) + ) + ) + ) + // end-comparison-aggregation + + val result = results.toList() + assertEquals(1, result.size) + assertEquals("California", result[0].getString("location")) + + collection.drop() + } + + @Test + fun conditionalOpsTest() { + + val collection = database.getCollection("memberData") + + val entries = listOf( + Document("name", "Sandra K").append("member", "Gold"), + Document("name", "Darren I").append("member", true), + Document("name", "Corey P").append("member", false), + Document("name", "Francine D").append("member", 7), + Document("name", "Lily A").append("member", listOf("None", "Gold", "Premium")) + ) + + collection.insertMany(entries) + + // start-conditional-aggregation + val member = current().getField("member") + + val results = collection.aggregate( + listOf( + Aggregates.project( + Projections.fields( + Projections.computed("membershipLevel", + member.switchOn { field -> + field + .isString { s -> s } + .isBoolean { b -> b.cond(of("Gold"), of("Guest")) } + .isArray { a -> a.last() } + .defaults { d -> of("Guest") } + }) + ) + ) + ) + ) + // end-conditional-aggregation + + val result = results.toList() + assertEquals(5, result.size) + assertEquals("Gold", result[0].getString("membershipLevel")) + assertEquals("Gold", result[1].getString("membershipLevel")) + assertEquals("Guest", result[2].getString("membershipLevel")) + assertEquals("Guest", result[3].getString("membershipLevel")) + assertEquals("Premium", result[4].getString("membershipLevel")) + + collection.drop() + } + + @Test + fun convenienceOpsTest() { + + data class Student(val name: String, val finalGrade: Int) + data class Class(val title: String, val students: List) + + val collection = database.getCollection("classData") + + val entries = listOf( + Class("History", listOf(Student("Kaley", 99), Student("Kevin", 80))), + Class("Math", listOf(Student("Dan", 68), Student("Shelley", 45))), + Class("Language Arts", listOf(Student("Kaley", 83), Student("Shelley", 86))) + ) + + collection.insertMany(entries) + + // start-convenience-aggregation-methods + fun gradeAverage(students: MqlArray, fieldName: String): MqlNumber { + val sum = students.sum { student -> student.getInteger(fieldName) } + val avg = sum.divide(students.size()) + return avg + } + + fun evaluate(grade: MqlNumber, cutoff1: MqlNumber, cutoff2: MqlNumber): MqlString { + val message = grade.switchOn { on -> + on + .lte(cutoff1) { g -> of("Needs improvement") } + .lte(cutoff2) { g -> of("Meets expectations") } + .defaults { g -> of("Exceeds expectations") } + } + return message + } + // end-convenience-aggregation-methods + + // start-convenience-aggregation + val students = current().getArray("students") + + val results = collection.aggregate( + listOf( + Aggregates.project( + Projections.fields( + Projections.computed("evaluation", students + .passArrayTo { s -> gradeAverage(s, "finalGrade") } + .passNumberTo { grade -> evaluate(grade, of(70), of(85)) }) + ) + ) + ) + ) + // end-convenience-aggregation + + val result = results.toList() + assertEquals(3, result.size) + assertEquals("Exceeds expectations", result[0].getString("evaluation")) + assertEquals("Needs improvement", result[1].getString("evaluation")) + assertEquals("Meets expectations", result[2].getString("evaluation")) + + collection.drop() + } + + @Test + fun conversionOpsTest() { + + data class Alumnus(val name: String, val graduationYear: String) + + val collection = database.getCollection("alumniData") + + val entries = listOf( + Alumnus("Shelley E", "2009"), + Alumnus("Courtney S", "1994") + ) + + collection.insertMany(entries) + + // start-conversion-aggregation + val graduationYear = current().getString("graduationYear") + + val results = collection.aggregate( + listOf( + Aggregates.addFields( + Field( + "reunionYear", + graduationYear + .parseInteger() + .add(5) + ) + ) + ) + ) + // end-conversion-aggregation + + val result = results.toList() + assertEquals(2, result.size) + assertEquals(2014, result[0].getInteger("reunionYear")) + assertEquals(1999, result[1].getInteger("reunionYear")) + + collection.drop() + } + + @Test + fun dateOpsTest() { + + data class Delivery(val desc: String, val deliveryDate: String) + + val collection = database.getCollection("deliveryData") + + val entries = listOf( + Delivery("Order #1234", "2018-01-15T16:00:00Z"), + Delivery("Order #4397", "Jan 15, 2018, 12:00 PM EST"), + Delivery("Order #4397", "Jun 8, 2023, 12:00 PM EST") + ) + + collection.insertMany(entries) + + // start-date-aggregation + val deliveryDate = current().getString("deliveryDate") + + val results = collection.aggregate( + listOf( + Aggregates.match( + Filters.expr( + deliveryDate + .parseDate() + .dayOfWeek(of("America/New_York")) + .eq(of(2)) + ) + ) + ) + ) + // end-date-aggregation + + val result = results.toList() + assertEquals(2, result.size) + assertEquals("Order #1234", result[0].getString("desc")) + assertEquals("Order #4397", result[1].getString("desc")) + + collection.drop() + } + + @Test + fun documentOpsTest() { + + val collection = database.getCollection("addressData") + + val address1 = Document("street", "601 Mongo Drive").append("city", "Vasqueztown").append("state", "CO") + .append("zip", 27017) + val address2 = + Document("street", "533 Maple Ave").append("city", "Bellevue").append("state", "WA").append("zip", 98004) + + val entries = listOf( + Document("customer.name", "Mary Kenneth Keller").append("mailing.address", address1), + Document("customer.name", "Surathi Raj").append("mailing.address", address2) + ) + + collection.insertMany(entries) + + // start-document-aggregation + val address = current().getDocument("mailing.address") + + val results = collection.aggregate( + listOf( + Aggregates.match( + Filters.expr( + address + .getString("state") + .eq(of("WA")) + ) + ) + ) + ) + // end-document-aggregation + + val result = results.toList() + assertEquals(1, result.size) + assertEquals("Surathi Raj", result[0].getString("customer.name")) + + collection.drop() + } + + @Test + fun mapOpsTest() { + + data class Item(val item: String, val warehouses: Map) + + val collection = database.getCollection("stockData") + + val doc = Item("notebook", mapOf("Atlanta" to 50, "Chicago" to 0, "Portland" to 120, "Dallas" to 6)) + + collection.insertOne(doc) + + // start-map-aggregation + val warehouses = current().getMap("warehouses") + + val results = collection.aggregate( + listOf( + Aggregates.project( + Projections.fields( + Projections.computed("totalInventory", warehouses + .entries() + .sum { v -> v.getValue() }) + ) + ) + ) + ) + // end-map-aggregation + + val result = results.toList() + assertEquals(1, result.size) + assertEquals(176, result[0].getInteger("totalInventory")) + + collection.drop() + } + + @Test + fun stringOpsTest() { + + data class Employee(val lastName: String, val employeeID: String) + + val collection = database.getCollection("employeeData") + + val entries = listOf( + Employee("Carter", "12w2"), + Employee("Derry", "32rj") + ) + + collection.insertMany(entries) + + // start-string-aggregation + val lastName = current().getString("lastName") + val employeeID = current().getString("employeeID") + + val results = collection.aggregate( + listOf( + Aggregates.project( + Projections.fields( + Projections.computed( + "username", lastName + .append(employeeID) + .toLower() + ) + ) + ) + ) + ) + // end-string-aggregation + + val result = results.toList() + assertEquals(2, result.size) + assertEquals("carter12w2", result[0].getString("username")) + assertEquals("derry32rj", result[1].getString("username")) + + collection.drop() + } + + @Test + fun typeOpsTest() { + + val collection = database.getCollection("movieRatingData") + + val entries = listOf( + Document("movie", "Narnia").append("rating", 4), + Document("movie", "Jaws").append("rating", null), + Document("movie", "Phantom Thread").append("rating", "hate!!") + ) + + collection.insertMany(entries) + + // start-type-aggregation + val rating = current().getField("rating") + + val results = collection.aggregate( + listOf( + Aggregates.project( + Projections.fields( + Projections.computed( + "numericalRating", rating + .isNumberOr(of(1)) + ) + ) + ) + ) + ) + // end-type-aggregation + + val result = results.toList() + assertEquals(3, result.size) + assertEquals(4, result[0].getInteger("numericalRating")) + assertEquals(1, result[1].getInteger("numericalRating")) + assertEquals(1, result[2].getInteger("numericalRating")) + + collection.drop() + } +} \ No newline at end of file diff --git a/source/index.txt b/source/index.txt index 8ecf273d..eba9c5d7 100644 --- a/source/index.txt +++ b/source/index.txt @@ -19,6 +19,7 @@ /read /indexes /aggregation + Use Aggregation Expression Operations /data-formats /faq /connection-troubleshooting @@ -85,6 +86,9 @@ Transform Your Data with Aggregation Learn how to use the {+driver-short+} to perform aggregation operations in the :ref:`kotlin-sync-aggregation` section. +Learn how to use aggregation expression operations to build +aggregation stages in the :ref:`kotlin-sync-aggregation-expression-operations` section. + Specialized Data Formats ------------------------