From fcae6287f00c761f0628441fc66b30dc6d3d639d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 21 Mar 2026 22:21:45 +0100 Subject: [PATCH] build(dsl-templator): The templator copies the file as-is, with a warning message --- dsl-template/README.md | 1 + .../kotlin/path/BsonPathConversions.kt | 7 +- dsl/src/commonMain/kotlin/BsonContext.kt | 51 + dsl/src/commonMain/kotlin/KtMongoDsl.kt | 26 + .../aggregation/AccumulationOperators.kt | 52 + .../aggregation/AggregationOperators.kt | 168 + .../commonMain/kotlin/aggregation/Pipeline.kt | 324 ++ .../kotlin/aggregation/PipelineType.kt | 83 + .../commonMain/kotlin/aggregation/Value.kt | 165 + .../ArithmeticValueAccumulators.kt | 397 ++ .../accumulators/ValueAccumulators.kt | 35 + .../operators/ArithmeticValueOperators.kt | 390 ++ .../operators/ArrayValueOperators.kt | 1429 +++++++ .../operators/ComparisonValueOperators.kt | 146 + .../operators/ConditionalValueOperators.kt | 177 + .../operators/StringValueOperators.kt | 1129 ++++++ .../operators/TrigonometryValueOperators.kt | 486 +++ .../operators/TypeValueOperators.kt | 552 +++ .../aggregation/operators/ValueOperators.kt | 248 ++ .../kotlin/aggregation/stages/Count.kt | 117 + .../kotlin/aggregation/stages/Group.kt | 99 + .../kotlin/aggregation/stages/Limit.kt | 94 + .../kotlin/aggregation/stages/Match.kt | 73 + .../kotlin/aggregation/stages/Project.kt | 279 ++ .../kotlin/aggregation/stages/Sample.kt | 71 + .../kotlin/aggregation/stages/Set.kt | 411 +++ .../kotlin/aggregation/stages/Skip.kt | 101 + .../kotlin/aggregation/stages/Sort.kt | 105 + .../kotlin/aggregation/stages/UnionWith.kt | 136 + .../kotlin/aggregation/stages/Unset.kt | 141 + .../commonMain/kotlin/command/BulkWrite.kt | 578 +++ dsl/src/commonMain/kotlin/command/Command.kt | 27 + dsl/src/commonMain/kotlin/command/Count.kt | 76 + dsl/src/commonMain/kotlin/command/Delete.kt | 123 + dsl/src/commonMain/kotlin/command/Drop.kt | 58 + dsl/src/commonMain/kotlin/command/Find.kt | 79 + dsl/src/commonMain/kotlin/command/Insert.kt | 115 + dsl/src/commonMain/kotlin/command/Replace.kt | 123 + dsl/src/commonMain/kotlin/command/Update.kt | 183 + .../kotlin/command/UpdateWithPipeline.kt | 207 ++ .../commonMain/kotlin/options/LimitOption.kt | 83 + dsl/src/commonMain/kotlin/options/MaxTime.kt | 67 + dsl/src/commonMain/kotlin/options/Options.kt | 211 ++ .../kotlin/options/ReadConcernOption.kt | 157 + .../kotlin/options/ReadPreferenceOption.kt | 153 + .../commonMain/kotlin/options/SkipOption.kt | 83 + .../commonMain/kotlin/options/SortOption.kt | 281 ++ .../kotlin/options/WriteConcernOption.kt | 385 ++ .../kotlin/path/BsonPathConversions.kt | 150 + dsl/src/commonMain/kotlin/path/Field.kt | 565 +++ dsl/src/commonMain/kotlin/path/Path.kt | 169 + .../kotlin/path/PropertyNameStrategy.kt | 109 + .../commonMain/kotlin/query/FilterQuery.kt | 3285 +++++++++++++++++ .../kotlin/query/FilterQueryImpl.kt | 325 ++ .../kotlin/query/FilterQueryPredicate.kt | 1028 ++++++ .../kotlin/query/FilterQueryPredicateImpl.kt | 420 +++ .../commonMain/kotlin/query/UpdateQuery.kt | 1276 +++++++ .../kotlin/query/UpdateQueryImpl.kt | 440 +++ .../kotlin/query/UpdateWithPipelineQuery.kt | 290 ++ dsl/src/commonMain/kotlin/tree/BsonNode.kt | 205 + .../kotlin/tree/CompoundBsonNode.kt | 141 + .../commonMain/kotlin/tree/CompoundNode.kt | 77 + dsl/src/commonMain/kotlin/tree/Node.kt | 92 + .../kotlin/utils/ImmutableWrapperList.kt | 32 + gradle/conventions/dsl-templator/README.md | 11 + .../src/main/kotlin/ApplyTemplateTask.kt | 80 + .../main/kotlin/KtMongoDslTemplatorPlugin.kt | 18 +- 67 files changed, 19190 insertions(+), 5 deletions(-) create mode 100644 dsl/src/commonMain/kotlin/BsonContext.kt create mode 100644 dsl/src/commonMain/kotlin/KtMongoDsl.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/AccumulationOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/AggregationOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/Pipeline.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/PipelineType.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/Value.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/accumulators/ArithmeticValueAccumulators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/accumulators/ValueAccumulators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/operators/ArithmeticValueOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/operators/ComparisonValueOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/operators/ConditionalValueOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/operators/StringValueOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/operators/TrigonometryValueOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/operators/TypeValueOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Count.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Group.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Limit.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Match.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Project.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Sample.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Set.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Skip.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Sort.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/UnionWith.kt create mode 100644 dsl/src/commonMain/kotlin/aggregation/stages/Unset.kt create mode 100644 dsl/src/commonMain/kotlin/command/BulkWrite.kt create mode 100644 dsl/src/commonMain/kotlin/command/Command.kt create mode 100644 dsl/src/commonMain/kotlin/command/Count.kt create mode 100644 dsl/src/commonMain/kotlin/command/Delete.kt create mode 100644 dsl/src/commonMain/kotlin/command/Drop.kt create mode 100644 dsl/src/commonMain/kotlin/command/Find.kt create mode 100644 dsl/src/commonMain/kotlin/command/Insert.kt create mode 100644 dsl/src/commonMain/kotlin/command/Replace.kt create mode 100644 dsl/src/commonMain/kotlin/command/Update.kt create mode 100644 dsl/src/commonMain/kotlin/command/UpdateWithPipeline.kt create mode 100644 dsl/src/commonMain/kotlin/options/LimitOption.kt create mode 100644 dsl/src/commonMain/kotlin/options/MaxTime.kt create mode 100644 dsl/src/commonMain/kotlin/options/Options.kt create mode 100644 dsl/src/commonMain/kotlin/options/ReadConcernOption.kt create mode 100644 dsl/src/commonMain/kotlin/options/ReadPreferenceOption.kt create mode 100644 dsl/src/commonMain/kotlin/options/SkipOption.kt create mode 100644 dsl/src/commonMain/kotlin/options/SortOption.kt create mode 100644 dsl/src/commonMain/kotlin/options/WriteConcernOption.kt create mode 100644 dsl/src/commonMain/kotlin/path/BsonPathConversions.kt create mode 100644 dsl/src/commonMain/kotlin/path/Field.kt create mode 100644 dsl/src/commonMain/kotlin/path/Path.kt create mode 100644 dsl/src/commonMain/kotlin/path/PropertyNameStrategy.kt create mode 100644 dsl/src/commonMain/kotlin/query/FilterQuery.kt create mode 100644 dsl/src/commonMain/kotlin/query/FilterQueryImpl.kt create mode 100644 dsl/src/commonMain/kotlin/query/FilterQueryPredicate.kt create mode 100644 dsl/src/commonMain/kotlin/query/FilterQueryPredicateImpl.kt create mode 100644 dsl/src/commonMain/kotlin/query/UpdateQuery.kt create mode 100644 dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt create mode 100644 dsl/src/commonMain/kotlin/query/UpdateWithPipelineQuery.kt create mode 100644 dsl/src/commonMain/kotlin/tree/BsonNode.kt create mode 100644 dsl/src/commonMain/kotlin/tree/CompoundBsonNode.kt create mode 100644 dsl/src/commonMain/kotlin/tree/CompoundNode.kt create mode 100644 dsl/src/commonMain/kotlin/tree/Node.kt create mode 100644 dsl/src/commonMain/kotlin/utils/ImmutableWrapperList.kt create mode 100644 gradle/conventions/dsl-templator/src/main/kotlin/ApplyTemplateTask.kt diff --git a/dsl-template/README.md b/dsl-template/README.md index 34967ecc..688cc2f2 100644 --- a/dsl-template/README.md +++ b/dsl-template/README.md @@ -60,3 +60,4 @@ Instead of developing the DSL module directly, all production code is written in On each build, the `:dsl` module is regenerated by copying this template module, adding all the necessary overloads in the process. The templating engine is implemented in the [dsl-templator](../gradle/conventions/dsl-templator) Gradle plugin. +It is executed by running `./gradlew :dsl:applyTemplate`. diff --git a/dsl-template/src/commonMain/kotlin/path/BsonPathConversions.kt b/dsl-template/src/commonMain/kotlin/path/BsonPathConversions.kt index 8cfb3ec4..60b2a00f 100644 --- a/dsl-template/src/commonMain/kotlin/path/BsonPathConversions.kt +++ b/dsl-template/src/commonMain/kotlin/path/BsonPathConversions.kt @@ -23,7 +23,6 @@ import opensavvy.ktmongo.bson.at import opensavvy.ktmongo.bson.select import opensavvy.ktmongo.bson.selectFirst import opensavvy.ktmongo.dsl.LowLevelApi -import org.bson.conversions.Bson /** * Converts this MongoDB [Path] to a [BsonPath]. @@ -85,7 +84,7 @@ fun Field<*, *>.toBsonPath(): BsonPath = this.path.toBsonPath() /** - * Finds all values that match [field] in a given [BSON document][Bson]. + * Finds all values that match [field] in a given [BsonDocument]. * * To learn more about the syntax, see [BsonPath]. * @@ -105,7 +104,7 @@ inline fun BsonDocument.select(field: Field<*, T>): Sequence = select(field.toBsonPath()) /** - * Finds the first value that matches [field] in a given [BSON document][Bson]. + * Finds the first value that matches [field] in a given [BsonDocument]. * * To learn more about the syntax, see [BsonPath]. * @@ -128,7 +127,7 @@ inline fun BsonDocument.selectFirst(field: Field<*, T>): T = selectFirst(field.toBsonPath()) /** - * Finds the first value that matches [path] in a given [BSON document][Bson]. + * Finds the first value that matches [path] in a given [BsonDocument]. * * ### Example * diff --git a/dsl/src/commonMain/kotlin/BsonContext.kt b/dsl/src/commonMain/kotlin/BsonContext.kt new file mode 100644 index 00000000..3e60b318 --- /dev/null +++ b/dsl/src/commonMain/kotlin/BsonContext.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/BsonContext.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl + +import opensavvy.ktmongo.bson.BsonFactory +import opensavvy.ktmongo.bson.types.ObjectIdGenerator +import opensavvy.ktmongo.dsl.path.PropertyNameStrategy +import kotlin.concurrent.atomics.ExperimentalAtomicApi + +/** + * BSON configuration for the objects generated by the DSL. + * + * This object is passed through the entire DSL to allow accessing the configuration from anywhere during request generation. + */ +interface BsonContext : BsonFactory, ObjectIdGenerator, PropertyNameStrategy + +private class BsonContextImpl( + bsonFactory: BsonFactory, + objectIdGenerator: ObjectIdGenerator, + nameStrategy: PropertyNameStrategy, +) : BsonContext, + BsonFactory by bsonFactory, + ObjectIdGenerator by objectIdGenerator, + PropertyNameStrategy by nameStrategy + +/** + * BSON configuration for the objects generated by the DSL. + */ +@ExperimentalAtomicApi +fun BsonContext( + bsonFactory: BsonFactory, + objectIdGenerator: ObjectIdGenerator = ObjectIdGenerator.Default(), + nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, +): BsonContext = BsonContextImpl(bsonFactory, objectIdGenerator, nameStrategy) diff --git a/dsl/src/commonMain/kotlin/KtMongoDsl.kt b/dsl/src/commonMain/kotlin/KtMongoDsl.kt new file mode 100644 index 00000000..dd6f7f97 --- /dev/null +++ b/dsl/src/commonMain/kotlin/KtMongoDsl.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/KtMongoDsl.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl + +/** + * Marker for functions that are part of the KtMongo DSL. + */ +@DslMarker +annotation class KtMongoDsl diff --git a/dsl/src/commonMain/kotlin/aggregation/AccumulationOperators.kt b/dsl/src/commonMain/kotlin/aggregation/AccumulationOperators.kt new file mode 100644 index 00000000..ebd2e06a --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/AccumulationOperators.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/AccumulationOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation + +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.aggregation.accumulators.ArithmeticValueAccumulators +import opensavvy.ktmongo.dsl.aggregation.accumulators.ValueAccumulators +import opensavvy.ktmongo.dsl.tree.AbstractCompoundBsonNode + +/** + * DSL to accumulate values into each other, available in the [`$group` stage][opensavvy.ktmongo.dsl.aggregation.stages.HasGroup.group]. + * + * Accumulation operators are a specific type of [aggregation operators][AggregationOperators]. + * + * ### Operators + * + * Arithmetic operators: + * - [`$avg`][ArithmeticValueAccumulators.average] + * - [`$median`][ArithmeticValueAccumulators.median] + * - [`$percentile`][ArithmeticValueAccumulators.percentiles] + * - [`$sum`][ArithmeticValueAccumulators.sum] + * + * @see Value Representation of an aggregation value. + * @see AggregationOperators Learn more about regular aggregation operators. + */ +@KtMongoDsl +interface AccumulationOperators : ValueAccumulators, + AggregationOperators, + ArithmeticValueAccumulators + +internal class AccumulationOperatorsImpl( + context: BsonContext, +) : AbstractCompoundBsonNode(context), + AccumulationOperators diff --git a/dsl/src/commonMain/kotlin/aggregation/AggregationOperators.kt b/dsl/src/commonMain/kotlin/aggregation/AggregationOperators.kt new file mode 100644 index 00000000..03c982b7 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/AggregationOperators.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/AggregationOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation + +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.aggregation.operators.* +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.query.FilterQuery + +/** + * DSL to instantiate aggregation values, available in most aggregation stages. + * + * ### What are aggregation values? + * + * In MongoDB, operators targeting regular queries and aggregation pipelines often have the same name but a different + * syntax. Using KtMongo, operators keep the same name __and syntax__ for both usages, but the way they are used in + * practice is still quite different. For example, compare [`$eq` (query)][FilterQuery.eq] + * and [`$eq` (aggregation)][ComparisonValueOperators.eq]. + * + * In regular queries, operators do not have a return type, and invoking them immediately adds them to the current query. + * If multiple operators are called within the operation lambda, each of them is added to the query: operators "bind" + * themselves on invoking. Operators always take a [field][Field] as first operand, and value as second operand. As an example: + * ```kotlin + * users.find { + * User::age gt 18 + * User::scores[0] eq 10 + * } + * ``` + * which generates: + * ```json + * { "$and": [{ "$eq": { "age": 18 } }, { "$eq": { "scores.0": 10 } }] } + * ``` + * + * In aggregations, operators have a return type and **only the last value of a block is taken into account**, just like + * in regular Kotlin code. We can use regular variables to store parts of a more complex expression. + * Operators accept multiple aggregation values which must conform to some type requirements. As an example: + * ```kotlin + * users.find { + * expr { // Use aggregation values in a regular find() request + * val maxScore = of(User::scores).max() + * maxScore lt of(15) + * } + * } + * ``` + * which generates: + * ```json + * { "expr": { "lt": [{ "$max": "$scores" }, { "$literal": 15 }] } } + * ``` + * + * As you can see, we use the [of][ValueOperators.of] method to convert from Kotlin values or from field names to aggregation values. + * Because each side of an operator accepts an aggregation value, we can thus compare multiple fields from the same document, + * use conditionals or other complex requests. + * + * In this example, we used the [`$expr`][FilterQuery.expr] query predicate to write an aggregation value within + * a regular query. `$expr` requires returning a boolean, so the last operator of the value needed to be a boolean-returning + * operator, which `$lt` is one of. In other contexts, aggregation values can be typed with any other document type. + * + * When writing your first aggregation pipelines, keep in mind that **only the last value of each lambda is used**, + * just like when calling Kotlin functions that return a value that is unused. + * + * ### Operators + * + * Access values: + * - [`$literal`][ValueOperators.of] + * + * Conditionally compute values: + * - [`$cond`][ConditionalValueOperators.cond] + * - [`$switch`][ConditionalValueOperators.switch] + * + * Compare values: + * - [`$eq`][ComparisonValueOperators.eq] + * - [`$ne`][ComparisonValueOperators.ne] + * - [`$gt`][ComparisonValueOperators.gt] + * - [`$lt`][ComparisonValueOperators.lt] + * - [`$gte`][ComparisonValueOperators.gte] + * - [`$lte`][ComparisonValueOperators.lte] + * + * Arithmetic operators: + * - [`$abs`][ArithmeticValueOperators.abs] + * - [`$add`][ArithmeticValueOperators.plus] + * - [`$ceil`][ArithmeticValueOperators.ceil] + * - [`$divide`][ArithmeticValueOperators.div] + * - [`$multiply`][ArithmeticValueOperators.times] + * - [`$subtract`][ArithmeticValueOperators.minus] + * + * Array operators: + * - [`$avg`][ArrayValueOperators.average] + * - [`$filter`][ArrayValueOperators.filter] + * - [`$firstN`][ArrayValueOperators.take] + * - [`$lastN`][ArrayValueOperators.takeLast] + * - [`$map`][ArrayValueOperators.map] + * - [`$sortArray`][ArrayValueOperators.sortedBy] + * + * Document operators: + * - [`$getField`][ValueOperators.div] + * + * String operators: + * - [`$concat`][StringValueOperators.concat] + * - [`$ltrim`][StringValueOperators.trimStart] + * - [`$replaceAll`][StringValueOperators.replace] + * - [`$replaceOne`][StringValueOperators.replaceFirst] + * - [`$rtrim`][StringValueOperators.trimEnd] + * - [`$split`][StringValueOperators.split] + * - [`$strLenBytes`][StringValueOperators.lengthUTF8] + * - [`$strLenCP`][StringValueOperators.length] + * - [`$substrBytes`][StringValueOperators.substringUTF8] + * - [`$substrCP`][StringValueOperators.substring] + * - [`$toLower`][StringValueOperators.lowercase] + * - [`$toUpper`][StringValueOperators.uppercase] + * - [`$trim`][StringValueOperators.trim] + * + * Type operators: + * - [`$type`][TypeValueOperators.type] + * - [`$isArray`][TypeValueOperators.isArray] + * - [`$isNumber`][TypeValueOperators.isNumber] + * - [`$toBoolean`][TypeValueOperators.toBoolean] + * - [`$toDate`][TypeValueOperators.toInstant] + * - [`$toDouble`][TypeValueOperators.toDouble] + * - [`$toInt`][TypeValueOperators.toInt] + * - [`$toLong`][TypeValueOperators.toLong] + * - [`$toObjectId`][TypeValueOperators.toObjectId] + * - [`$toString`][TypeValueOperators.toText] + * - [`$toUUID`][TypeValueOperators.toUuid] + * + * Trigonometric operators and angle management: + * - [`$acos`][TrigonometryValueOperators.acos] + * - [`$acosh`][TrigonometryValueOperators.acosh] + * - [`$asin`][TrigonometryValueOperators.asin] + * - [`$asinh`][TrigonometryValueOperators.asinh] + * - [`$atan`][TrigonometryValueOperators.atan] + * - [`$atanh`][TrigonometryValueOperators.atanh] + * - [`$cos`][TrigonometryValueOperators.cos] + * - [`$cosh`][TrigonometryValueOperators.cosh] + * - [`$sin`][TrigonometryValueOperators.sin] + * - [`$sinh`][TrigonometryValueOperators.sinh] + * - [`$tan`][TrigonometryValueOperators.tan] + * - [`$tanh`][TrigonometryValueOperators.tanh] + * - [`$degreesToRadians`][TrigonometryValueOperators.toRadians] + * - [`$radiansToDegrees`][TrigonometryValueOperators.toDegrees] + * + * @see Value Representation of an aggregation value. + */ +@KtMongoDsl +interface AggregationOperators : ValueOperators, + ArrayValueOperators, + ComparisonValueOperators, + ConditionalValueOperators, + ArithmeticValueOperators, + StringValueOperators, + TrigonometryValueOperators, + TypeValueOperators diff --git a/dsl/src/commonMain/kotlin/aggregation/Pipeline.kt b/dsl/src/commonMain/kotlin/aggregation/Pipeline.kt new file mode 100644 index 00000000..7862925f --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/Pipeline.kt @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/Pipeline.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation + +import opensavvy.ktmongo.bson.BsonDocument +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import opensavvy.ktmongo.dsl.tree.AbstractCompoundBsonNode +import opensavvy.ktmongo.dsl.tree.BsonNode +import opensavvy.ktmongo.dsl.tree.CompoundBsonNode + +/** + * A multi-stage pipeline that performs complex operations on MongoDB. + * + * Similar to [Sequence] and [Flow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/), + * but executed by MongoDB itself. + * + * MongoDB has different types of pipelines with different available operators, which are represented by the different + * implementations of this interface. + * + * Instances of this class are immutable. + * + * ### Stages + * + * A pipeline is composed of _stages_, each of which transforms the data in some way. + * For example, some stages filter information out, some stages add more information, some stages combine documents, + * some stages extract information from elsewhere, etc. + * + * Each stage is defined as an extension function on this class. + * Note that as mentioned, not all stages are available for all pipeline types. + * The following stages are available: + * - [`$limit`][opensavvy.ktmongo.dsl.aggregation.stages.HasLimit.limit] + * - [`$match`][opensavvy.ktmongo.dsl.aggregation.stages.HasMatch.match] + * - [`$project`][opensavvy.ktmongo.dsl.aggregation.stages.HasProject.project] + * - [`$sample`][opensavvy.ktmongo.dsl.aggregation.stages.HasSample.sample] + * - [`$set`][opensavvy.ktmongo.dsl.aggregation.stages.HasSet.set] + * - [`$skip`][opensavvy.ktmongo.dsl.aggregation.stages.HasSkip.skip] + * - [`$sort`][opensavvy.ktmongo.dsl.aggregation.stages.HasSort.sort] + * - [`$unionWith`][opensavvy.ktmongo.dsl.aggregation.stages.HasUnionWith.unionWith] + * - [`$unset`][opensavvy.ktmongo.dsl.aggregation.stages.HasUnset.unset] + * + * If you can't find a stage you're searching for, visit the [tracking issue](https://gitlab.com/opensavvy/ktmongo/-/issues/7). + * + * ### Implementing a new stage + * + * Just like operators, stages can be added as extension methods on this type or any of its subtypes. + * To register the stage, call [withStage], optionally followed by [reinterpret], and return the resulting pipeline. + * + * Stages should return [Pipeline] instances **that were generated by the [withStage] or [reinterpret] methods**. + * [Pipeline] implementations are allowed to assume all stages they will be provided were generated by their own + * implementation of these methods, and thus may downcast the resulting pipeline to another type safely. + * Returning any other [Pipeline] instance has unspecified behavior. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/aggregation/) + * + * @param Output The type of document that this pipeline results in. Changing this type is possible by calling [reinterpret]. + */ +@KtMongoDsl +interface Pipeline { + + /** + * The context used to generate this pipeline. + * + * Can be accessed within children expressions. + */ + @LowLevelApi + val context: BsonContext + + /** + * Creates a new pipeline that expands on the current one by adding [stage]. + * + * This method is analogous to [CompoundBsonNode.accept], with the main difference that the latter mutates the + * current expression, whereas this method returns a new pipeline on which the stage is applied + * (because pipelines are immutable). + * + * **End-users should not need to call this function.** + * All implemented stages provide an extension function on the [Pipeline] type. + * This function is provided for cases in which you need a stage that is not yet provided by the library. + * If that is your situation, start by reading [AbstractBsonNode] and [AbstractCompoundBsonNode]. + * If you want to proceed and implement your own stage, consider getting in touch with the maintainers of the + * library so it can be shared to all users. + * + * The provided [stage] must validate the entire contract of [BsonNode]. Additionally, it should always emit + * the name of the stage first. For example, this is a valid stage: + * ```json + * "$match": { + * "name": "Bob" + * } + * ``` + * but this isn't: + * ```json + * "name": "Bob" + * ``` + * because it doesn't start with a stage name. + * + * Similarly, this isn't a valid stage, because it declares two different stage names: + * ```json + * "$match": { + * "name": "Bob" + * }, + * "$set": { + * "foo": "bar" + * } + * ``` + * + * @see reinterpret Change the output type of this pipeline. + */ + @DangerousMongoApi + @LowLevelApi + fun withStage(stage: BsonNode): Pipeline + + /** + * Changes the type of the returned document, with no type-safety. + * + * **End-users should not need to call this function.** + * This function is provided to allow stages to change the return document. + * No type verifications are made, it is solely the responsibility of the caller to ensure that the declared return + * type corresponds to the reality. + * + * @see withStage Add a new stage to this pipeline. + */ + @Suppress("UNCHECKED_CAST") + @DangerousMongoApi + @LowLevelApi + fun reinterpret(): Pipeline + + /** + * Writes the entire pipeline into [writer]. + * + * This function is similar to [BsonNode.writeTo], with the difference that expressions generate documents, + * and pipelines generate arrays. + * + * Using this method will thus write an array containing the different stages. + */ + @LowLevelApi + fun writeTo(writer: BsonValueWriter) + + /** + * JSON representation of this pipeline. + */ + override fun toString(): String + +} + +/** + * A single link in the [Pipeline] chain. + * + * **End-users should not interact with this class.** + * This class is provided as an implementation detail of [AbstractPipeline]. + * If you are not implementing your own pipeline type, you do not need to interact with this class at all. + */ +@LowLevelApi +class PipelineChainLink internal constructor( + private val context: BsonContext, + private val previous: PipelineChainLink?, + private val current: BsonNode?, +) { + + /** + * Creates an empty [PipelineChainLink], corresponding to an empty aggregation pipeline. + */ + constructor( + context: BsonContext, + ) : this(context, null, null) + + /** + * Equivalent to [Pipeline.withStage], but generating a chain link instead. + */ + fun withStage(stage: BsonNode): PipelineChainLink { + val simplified = stage.simplify() ?: return this + simplified.freeze() + return PipelineChainLink(context, this, simplified) + } + + /** + * Iterates through this chain. + * + * The first returned element is the current one, the second returned element is the previous one, + * the third element is the previous one, etc. + */ + private fun hierarchyReversed(): Sequence = sequence { + var cursor: PipelineChainLink? = this@PipelineChainLink + + while (cursor != null) { + if (cursor.current != null) + yield(cursor.current) + + cursor = cursor.previous + } + } + + /** + * Converts this chain to a list of expressions. + * + * The first element of the returned list is the root of the chain, followed by the second element of the chain, etc. + */ + @LowLevelApi + fun toList(): List = + hierarchyReversed().toList().reversed() + + /** + * Converts this chain in a list of BSON documents, each representing a stage. + * + * The first element of the returned list is the root of the chain, followed by the second element of the chain, etc. + */ + @LowLevelApi + fun toBsonList(): List = + hierarchyReversed() + .map { context.buildDocument { it.writeTo(this) } } + .toList().reversed() + + /** + * Equivalent to [Pipeline.writeTo]. + */ + @LowLevelApi + fun writeTo(writer: BsonValueWriter) = with(writer) { + val stages = hierarchyReversed().toList().reversed() + + for (stage in stages) { + writeDocument { + stage.writeTo(this) + } + } + } + + /** + * JSON representation of this pipeline. + */ + @OptIn(LowLevelApi::class) + override fun toString(): String = context.buildArray { + writeTo(this) + }.toString() + +} + +/** + * Helper class to implement [Pipeline]. + * + * ### Notes for implementors + * + * When implementing a new type of pipeline, the main requirement is to override the return tpe of all existing stage + * methods to return the same type as the current instance. This sadly has to be done manually because Kotlin doesn't + * have self-types. + * + * When overriding the stage methods, avoid doing anything other than down-casting the resulting pipeline. + * + * You will also need to implement [withStage]. + * Note how creating an instance of [AbstractPipeline] requires passing a [PipelineChainLink]. + * [PipelineChainLink] implements all complex methods from [Pipeline] for you. + */ +abstract class AbstractPipeline @OptIn(LowLevelApi::class) constructor( + + @property:LowLevelApi + override val context: BsonContext, + + /** + * Internal representation of the pipeline state. + */ + @property:LowLevelApi + val chain: PipelineChainLink, +) : Pipeline { + + /** + * Creates a new pipeline that expands on the current one by adding [stage]. + * + * For usage documentation, see [Pipeline.withStage]. + * + * ### Notes for implementors + * + * A typical pipeline implementation will look like: + * ```kotlin + * class YourPipelineType( + * context: BsonContext, + * chain: PipelineChainLink, + * ): AbstractPipeline(context, chain) { + * // … + * + * override fun withStage(stage: Expression): YourPipelineType = + * YourPipelineType(context, chain.withStage(expression)) + * + * // … + * } + * ``` + */ + @DangerousMongoApi + @LowLevelApi + abstract override fun withStage(stage: BsonNode): Pipeline + + @LowLevelApi + final override fun writeTo(writer: BsonValueWriter) { + chain.writeTo(writer) + } + + /** + * JSON representation of this pipeline. + */ + @OptIn(LowLevelApi::class) + final override fun toString(): String = + chain.toString() + +} diff --git a/dsl/src/commonMain/kotlin/aggregation/PipelineType.kt b/dsl/src/commonMain/kotlin/aggregation/PipelineType.kt new file mode 100644 index 00000000..14d1cec4 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/PipelineType.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/PipelineType.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation + +import opensavvy.ktmongo.dsl.aggregation.stages.* + +/** + * An aggregation pipeline. + * + * Aggregation pipelines read data from one or more collections and transform it in a manner of ways. + * Finally, the data can be sent to the server, or written to another collection. + * + * ### Example + * + * ```kotlin + * invoices.aggregate() + * .match { Invoice::isDraft eq false } + * .set { + * Invoice::anomaly set (of(Invoice::modificationDate) lt of(Invoice::creationDate)) + * } + * .sort { ascending(Invoice::creationDate) } + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/core/aggregation-pipeline/) + */ +interface AggregationPipeline : Pipeline, + HasCount, + HasGroup, + HasLimit, + HasMatch, + HasProject, + HasSample, + HasSet, + HasSkip, + HasSort, + HasUnionWith, + HasUnionWithCompatibility, + HasUnset + +/** + * An update pipeline. + * + * Update pipelines allow more complex updates directly through the various `update` functions. + * + * ### Example + * + * ```kotlin + * users.updateManyWithPipeline { + * set { + * User::score set (of(User::score) + (of(User::scoreMultiplier) * of(User::dailyScore))) + * User::dailyScore set 0 + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/#update-with-aggregation-pipeline) + */ +interface UpdatePipeline : Pipeline, + HasProject, + HasSet, + HasUnset diff --git a/dsl/src/commonMain/kotlin/aggregation/Value.kt b/dsl/src/commonMain/kotlin/aggregation/Value.kt new file mode 100644 index 00000000..7eb0b753 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/Value.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/Value.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.bson.BsonValueWriteable +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import opensavvy.ktmongo.dsl.tree.BsonNode +import opensavvy.ktmongo.dsl.tree.Node +import opensavvy.ktmongo.dsl.tree.NodeImpl + +/** + * An intermediary value in an aggregation expression. + * + * Each implementation of this interface is a logical BSON node in our own intermediate representation. + * Each node knows how to [writeTo] itself into a BSON document. + * + * Instances of this interface are obtained by the end-user through the [AggregationOperators] builder. + * Functions from KtMongo which expect aggregation values provide an instance of [AggregationOperators] into scope automatically. + * For example, see [FilterQuery.expr]. + * + * ### Difference with Expression + * + * This interface and its hierarchy mimic [BsonNode]. + * The main difference is the expected context: [BsonNode] represents an operator, which is stored as a BSON document + * and doesn't participate in any type hierarchy. + * Instead, [Value] is stored as a BSON value and its return type can be further embedded into more values. + * + * ### Security + * + * Implementing this interface allows injecting arbitrary BSON into a request. + * Be very careful not to make injections possible. + * + * ### Implementation notes + * + * Prefer implementing [AbstractValue] instead of implementing this interface directly. + * + * ### Debugging notes + * + * Use [toString] to view the JSON representation of this expression. + * + * @see AggregationOperators Builder for aggregation values. + */ +interface Value : Node, BsonValueWriteable { + + /** + * The context used to generate this value. + */ + @LowLevelApi + val context: BsonContext + + /** + * Makes this value immutable. + * + * After this method has been called, the value can never be modified again. + * This ensures that values cannot change after they have been used within other values. + */ + @LowLevelApi + override fun freeze() + + /** + * Returns a simplified (but equivalent) value to the current value. + */ + @LowLevelApi + fun simplify(): Value + + /** + * Writes the result of [simplifying][simplify] this value into [writer]. + */ + @LowLevelApi + override fun writeTo(writer: BsonValueWriter) + + /** + * JSON representation of this expression. + * + * Note that since this class represents a BSON _value_, and BSON libraries often only support _documents_, + * the actual value may be surrounded by some boilerplate (like an array or a useless value). + */ + override fun toString(): String +} + +/** + * Utility implementation of [Value], which handles the [context], [toString] representation and [freezing][freeze]. + * + * ### Implementing a new operator + * + * Implementing class is identical in concept to implementing [AbstractBsonNode]. + * The main difference is the writer is a [BsonValueWriter] instead of a [BsonFieldWriter]. + */ +@LowLevelApi +abstract class AbstractValue private constructor( + @property:LowLevelApi override val context: BsonContext, + private val node: NodeImpl, +) : Node by node, Value { + + constructor(context: BsonContext) : this(context, NodeImpl()) + + /** + * `true` if [freeze] has been called. Can never become `false` again. + * + * If this value is `true`, this value should reject any attempt to mutate it. + * It is the responsibility of the implementor to satisfy this invariant. + */ + protected val frozen: Boolean + get() = node.frozen + + /** + * Called when the value should be written to a [writer]. + * + * Note that this function is only called on instances that have already passed through [simplify], + * so it is guaranteed that this value is fully simplified already. + */ + @LowLevelApi + protected abstract fun write(writer: BsonValueWriter) + + @LowLevelApi + override fun simplify(): AbstractValue = this + + @LowLevelApi + final override fun writeTo(writer: BsonValueWriter) { + this.simplify().write(writer) + } + + /** + * JSON representation of this expression. + * + * By default, simplifications are enabled. Set [simplified] to `false` to disable simplifications. + */ + @OptIn(LowLevelApi::class) + fun toString(simplified: Boolean): String { + val document = context.buildArray { + if (simplified) + writeTo(this) + else + write(this) + } + + return document.toString() + } + + final override fun toString(): String = + toString(simplified = true) + +} diff --git a/dsl/src/commonMain/kotlin/aggregation/accumulators/ArithmeticValueAccumulators.kt b/dsl/src/commonMain/kotlin/aggregation/accumulators/ArithmeticValueAccumulators.kt new file mode 100644 index 00000000..65163683 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/accumulators/ArithmeticValueAccumulators.kt @@ -0,0 +1,397 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/accumulators/ArithmeticValueAccumulators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.accumulators + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AccumulationOperators +import opensavvy.ktmongo.dsl.aggregation.Value +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.path.Path +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import kotlin.reflect.KProperty1 + +/** + * Accumulators to perform arithmetic operations. + * + * To learn more about accumulation operators, see [AccumulationOperators]. + */ +@KtMongoDsl +interface ArithmeticValueAccumulators : ValueAccumulators { + + // region $sum + + /** + * Calculates and returns the collective sum of numeric values. + * Non-numeric values are ignored. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val balance: Int, + * ) + * + * class Result( + * val totalBalance: Int, + * ) + * + * users.aggregate() + * .group { + * Result::totalBalance sum of(User::balance) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/#mongodb-group-grp.-sum) + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes T : Number> Field.sum(value: Value) { + accept(ArithmeticValueAccumulator("\$sum", value, this.path, context)) + } + + /** + * Calculates and returns the collective sum of numeric values. + * Non-numeric values are ignored. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val balance: Int, + * ) + * + * class Result( + * val totalBalance: Int, + * ) + * + * users.aggregate() + * .group { + * Result::totalBalance sum of(User::balance) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/#mongodb-group-grp.-sum) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes T : Number> KProperty1.sum(value: Value) { + this.field.sum(value) + } + + // endregion + // region $avg + + /** + * Calculates and returns the collective average of numeric values. + * Non-numeric values are ignored. + * + * If all elements are non-numeric, `null` is returned. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val balance: Int, + * ) + * + * class Result( + * val totalBalance: Int, + * ) + * + * users.aggregate() + * .group { + * Result::totalBalance average of(User::balance) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg) + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes T : Number> Field.average(value: Value) { + accept(ArithmeticValueAccumulator("\$avg", value, this.path, context)) + } + + /** + * Calculates and returns the collective average of numeric values. + * Non-numeric values are ignored. + * + * If all elements are non-numeric, `null` is returned. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val balance: Int, + * ) + * + * class Result( + * val totalBalance: Int, + * ) + * + * users.aggregate() + * .group { + * Result::totalBalance average of(User::balance) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes T : Number> KProperty1.average(value: Value) { + this.field.average(value) + } + + // endregion + // region $median + + /** + * Returns an approximation of the median, the 50th percentile, as a scalar value. + * + * The median is computed with the [t-digest algorithm](https://arxiv.org/abs/1902.04023), which computes an approximation. + * The result may vary, even on the same dataset. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val balance: Int, + * ) + * + * class Result( + * val medianBalance: Double, + * ) + * + * users.aggregate() + * .group { + * Result::medianBalance median of(User::balance) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/median/) + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes T : Number> Field.median(value: Value) { + accept(MedianValueAccumulator(value, this.path, context)) + } + + /** + * Returns an approximation of the median, the 50th percentile, as a scalar value. + * + * The median is computed with the [t-digest algorithm](https://arxiv.org/abs/1902.04023), which computes an approximation. + * The result may vary, even on the same dataset. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val balance: Int, + * ) + * + * class Result( + * val medianBalance: Double, + * ) + * + * users.aggregate() + * .group { + * Result::medianBalance median of(User::balance) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/median/) + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes T : Number> KProperty1.median(value: Value) { + this.field median value + } + + // endregion + // region $percentile + + /** + * Returns an approximation of the specified [percentiles]. + * + * Each percentile is computed with the [t-digest algorithm](https://arxiv.org/abs/1902.04023), which computes an approximation. + * The results may vary, even on the same dataset. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val balance: Int, + * ) + * + * class Result( + * val percentiles: List, + * ) + * + * users.aggregate() + * .group { + * Result::percentiles.percentiles(of(User::balance), 0.5, 0.75, 0.9, 0.95) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/percentiles/) + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes T : Number> Field>.percentiles( + value: Value, + vararg percentiles: Double, + ) { + accept(PercentileValueAccumulator(value, this.path, percentiles.asList(), context)) + } + + /** + * Returns an approximation of the specified [percentiles]. + * + * Each percentile is computed with the [t-digest algorithm](https://arxiv.org/abs/1902.04023), which computes an approximation. + * The results may vary, even on the same dataset. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val balance: Int, + * ) + * + * class Result( + * val percentiles: List, + * ) + * + * users.aggregate() + * .group { + * Result::percentiles.percentiles(of(User::balance), 0.5, 0.75, 0.9, 0.95) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/percentiles/) + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes T : Number> KProperty1>.percentiles( + value: Value, + vararg percentiles: Double, + ) { + this.field.percentiles(value, percentiles = percentiles) + } + + // endregion + + @LowLevelApi + private class ArithmeticValueAccumulator( + val operator: String, + val value: Value<*, *>, + val into: Path, + context: BsonContext, + ) : AbstractBsonNode(context) { + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument(into.toString()) { + write(operator) { + value.writeTo(this) + } + } + } + } + + @LowLevelApi + private class MedianValueAccumulator( + val value: Value<*, *>, + val into: Path, + context: BsonContext, + ) : AbstractBsonNode(context) { + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument(into.toString()) { + writeDocument($$"$median") { + write("input") { + value.writeTo(this) + } + writeString("method", "approximate") + } + } + } + } + + @LowLevelApi + private class PercentileValueAccumulator( + val value: Value<*, *>, + val into: Path, + val percentiles: List, + context: BsonContext, + ) : AbstractBsonNode(context) { + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument(into.toString()) { + writeDocument($$"$percentile") { + write("input") { + value.writeTo(this) + } + writeString("method", "approximate") + writeArray("p") { + percentiles.forEach { percentile -> + writeDouble(percentile) + } + } + } + } + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/accumulators/ValueAccumulators.kt b/dsl/src/commonMain/kotlin/aggregation/accumulators/ValueAccumulators.kt new file mode 100644 index 00000000..4c342971 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/accumulators/ValueAccumulators.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/accumulators/ValueAccumulators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.accumulators + +import opensavvy.ktmongo.dsl.aggregation.AccumulationOperators +import opensavvy.ktmongo.dsl.aggregation.operators.ValueOperators +import opensavvy.ktmongo.dsl.tree.CompoundBsonNode + +/** + * Supertype for all interface operators describing accumulation operators. + * + * To register a custom accumulator, use [accept]. + * + * Most of the time, end-users will be using the subtype [AccumulationOperators] instead of this interface. + * + * Because accumulation operators are valued from aggregation operators, this interface extends [ValueOperators]. + */ +interface ValueAccumulators : ValueOperators, CompoundBsonNode diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ArithmeticValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ArithmeticValueOperators.kt new file mode 100644 index 00000000..bba9bb0f --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ArithmeticValueOperators.kt @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/operators/ArithmeticValueOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.operators + +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AbstractValue +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Value + +/** + * Operators to arithmetically combine two or more values. + * + * To learn more about aggregation operators, see [AggregationOperators]. + */ +interface ArithmeticValueOperators : ValueOperators { + + // region $abs + + /** + * The absolute value of a number. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Sensor( + * val name: String, + * val startTemp: Int, + * val endTemp: Int, + * val diffTemp: Int, + * ) + * + * collection.updateManyWithPipeline(filter = { Sensor::diffTemp.isNull() }) { + * set { + * Sensor::diffTemp set abs(of(Sensor::startTemp) - of(Sensor::endTemp)) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/abs/) + */ + @OptIn(LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun abs(value: Value): Value = + UnarySameTypeValueOperator(context, "abs", value) + + // endregion + // region $add + + /** + * Sums two aggregation values. + * + * ### Example + * + * ```kotlin + * class Product( + * val name: String, + * val price: Int, + * val dailyPriceIncrease: Int, + * ) + * + * collection.updateManyWithPipeline { + * set { + * Product::price set (of(Product::price) + of(Product::dailyPriceIncrease)) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/add/) + */ + @OptIn(LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + operator fun Value.plus(other: Value): Value = + AdditionValueOperator(context, listOf(this, other)) + + @OptIn(LowLevelApi::class) + private class AdditionValueOperator( + context: BsonContext, + private val operands: List>, + ) : AbstractValue(context) { + + override fun simplify(): AbstractValue { + val flattenedOperands = ArrayList>() + + for (operand in operands) { + if (operand is AdditionValueOperator) { + flattenedOperands += operand.operands + } else { + flattenedOperands += operand + } + } + + return if (flattenedOperands != operands) { + AdditionValueOperator(context, flattenedOperands) + } else { + this + } + } + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$add") { + for (operand in operands) { + operand.writeTo(this) + } + } + } + } + } + + // endregion + // region $ceil + + /** + * The smallest integer greater than or equal to the specified [value]. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Sensor( + * val value: Double, + * val maxBound: Double, + * ) + * + * collection.aggregate() + * .set { + * Sensor::maxBound set ceil(of(Sensor::value)) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/ceil/) + */ + @OptIn(LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun ceil(value: Value): Value = + UnarySameTypeValueOperator(context, "ceil", value) + + // endregion + // region $multiply + + /** + * Multiplies two or more aggregation values. + * + * ### Example + * + * ```kotlin + * class Sale( + * val price: Double, + * val quantity: Int, + * val total: Double, + * ) + * + * collection.updateManyWithPipeline { + * set { + * Sale::total set (of(Sale::price) * of(Sale::quantity)) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/multiply/) + */ + @OptIn(LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + operator fun Value.times(other: Value): Value = + MultiplicationValueOperator(context, listOf(this, other)) + + @OptIn(LowLevelApi::class) + private class MultiplicationValueOperator( + context: BsonContext, + private val operands: List>, + ) : AbstractValue(context) { + + override fun simplify(): AbstractValue { + val flattenedOperands = ArrayList>() + + for (operand in operands) { + if (operand is MultiplicationValueOperator) { + flattenedOperands += operand.operands + } else { + flattenedOperands += operand + } + } + + return if (flattenedOperands != operands) { + MultiplicationValueOperator(context, flattenedOperands) + } else { + this + } + } + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$multiply") { + for (operand in operands) { + operand.writeTo(this) + } + } + } + } + } + + // endregion + // region $divide + + /** + * Divides one aggregation value by another. + * + * ### Example + * + * ```kotlin + * class ConferencePlanning( + * val hours: Int, + * val workdays: Double, + * ) + * + * collection.updateManyWithPipeline { + * set { + * ConferencePlanning::workdays set (of(ConferencePlanning::hours) / of(8)) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/divide/) + */ + @OptIn(LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + operator fun Value.div(other: Value): Value = + DivisionValueOperator(context, this, other) + + @OptIn(LowLevelApi::class) + private class DivisionValueOperator( + context: BsonContext, + private val dividend: Value, + private val divisor: Value, + ) : AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$divide") { + dividend.writeTo(this) + divisor.writeTo(this) + } + } + } + } + + // endregion + // region $subtract + + /** + * Subtracts one aggregation value from another. + * + * The second argument is subtracted from the first argument. + * + * ### Example + * + * ```kotlin + * class Sale( + * val price: Int, + * val fee: Int, + * val discount: Int, + * val total: Int, + * ) + * + * collection.updateManyWithPipeline { + * set { + * Sale::total set (of(Sale::price) + of(Sale::fee) - of(Sale::discount)) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/subtract/) + */ + @OptIn(LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + operator fun Value.minus(other: Value): Value = + SubtractionValueOperator(context, this, other) + + @OptIn(LowLevelApi::class) + private class SubtractionValueOperator( + context: BsonContext, + private val minuend: Value, + private val subtrahend: Value, + ) : AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$subtract") { + minuend.writeTo(this) + subtrahend.writeTo(this) + } + } + } + } + + // endregion + // region $floor + + /** + * The largest integer less than or equal to the specified [value]. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Sensor( + * val value: Double, + * val minBound: Double, + * ) + * + * collection.aggregate() + * .set { + * Sensor::minBound set floor(of(Sensor::value)) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/floor/) + */ + @OptIn(LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun floor(value: Value): Value = + UnarySameTypeValueOperator(context, "floor", value) + + // endregion + + @LowLevelApi + private class UnarySameTypeValueOperator( + context: BsonContext, + private val operator: String, + private val value: Value, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + write("$$operator") { + value.writeTo(this) + } + } + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt new file mode 100644 index 00000000..80b0394e --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt @@ -0,0 +1,1429 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.operators + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AbstractValue +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Value +import opensavvy.ktmongo.dsl.options.SortOptionDsl +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.path.Path +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import opensavvy.ktmongo.dsl.tree.AbstractCompoundBsonNode +import kotlin.reflect.KProperty1 + +/** + * Operators to manipulate arrays. + * + * To learn more about aggregation operators, see [opensavvy.ktmongo.dsl.aggregation.AggregationOperators]. + */ +interface ArrayValueOperators : ValueOperators { + + // region $avg + + /** + * Returns the average of the elements in the array. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val averageScore: Double, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::averageScore set Player::scores.average() + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value>.average(): Value = + AverageArrayValueOperator( + input = this, + context = context, + ) + + /** + * Returns the average of the elements in the array. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val averageScore: Double, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::averageScore set Player::scores.average() + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Field>.average(): Value = + AverageArrayValueOperator( + input = of(this), + context = context, + ) + + /** + * Returns the average of the elements in the array. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val averageScore: Double, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::averageScore set Player::scores.average() + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun KProperty1>.average(): Value = + AverageArrayValueOperator( + input = of(this), + context = context, + ) + + @LowLevelApi + private class AverageArrayValueOperator( + private val input: Value>, + context: BsonContext, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + write("\$avg") { + input.writeTo(this) + } + } + } + } + + /** + * Returns the average of the elements in the array. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val averageScore: Double, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::averageScore set Player::scores.average() + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Iterable>.average(): Value = + AverageOfValueOperator( + input = this.toList(), + context = context, + ) + + /** + * Returns the average of the elements in the array. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val averageScore: Double, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::averageScore set Player::scores.average() + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun average(vararg input: Value): Value = + AverageOfValueOperator( + input = input.asList(), + context = context, + ) + + @Deprecated("Computing the average of 0 elements makes no sense, you should specify the elements to average as the receiver or as arguments.", level = DeprecationLevel.ERROR) + @KtMongoDsl + fun average(): Value = + error("Computing the average of 0 elements makes no sense, did you forget to specify arguments?") + + @LowLevelApi + private class AverageOfValueOperator( + private val input: List>, + context: BsonContext, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$avg") { + for (document in input) { + document.writeTo(this) + } + } + } + } + } + + // endregion + // region $filter + + /** + * Selects a subset of an array to return based on the specified [predicate], similarly to [Kotlin's `filter`][kotlin.collections.filter]. + * + * The returned elements are in the original order. + * + * ### Example + * + * ```kotlin + * class Sensor( + * val measurements: List, + * ) + * + * collection.updateManyWithPipeline { + * set { + * Sensor::measurements set (Sensor::measurements).filter { it gte of(0) } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/) + * + * @param limit If set, specifies a maximum number of elements returned: + * only the first [limit] matching elements are returned, even if there are more matching elements. + * Must be greater or equal to `1`, or be `null`. + * + * @param variableName The name of the temporary variable passed to the [predicate] lambda, which represents the + * current element being iterated over. By default, `"this"`. Setting this parameter is only useful when using + * nested [filter] or other similar calls, which could otherwise conflict. + */ + @OptIn(LowLevelApi::class) + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun Value>.filter( + limit: Value? = null, + variableName: String = "this", + predicate: AggregationOperators.(Value) -> Value, + ): Value> = + FilterValueOperator( + input = this, + predicate = PredicateEvaluator(context).predicate(ThisValue(variableName, context)), + limit = limit, + variableName = variableName, + context = context, + ) + + /** + * Selects a subset of an array to return based on the specified [predicate], similarly to [Kotlin's `filter`][kotlin.collections.filter]. + * + * The returned elements are in the original order. + * + * ### Example + * + * ```kotlin + * class Sensor( + * val measurements: List, + * ) + * + * collection.updateManyWithPipeline { + * set { + * Sensor::measurements set (Sensor::measurements).filter { it gte of(0) } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/) + * + * @param limit If set, specifies a maximum number of elements returned: + * only the first [limit] matching elements are returned, even if there are more matching elements. + * Must be greater or equal to `1`, or be `null`. + * + * @param variableName The name of the temporary variable passed to the [predicate] lambda, which represents the + * current element being iterated over. By default, `"this"`. Setting this parameter is only useful when using + * nested [filter] or other similar calls, which could otherwise conflict. + */ + @KtMongoDsl + fun Field>.filter( + limit: Value? = null, + variableName: String = "this", + predicate: AggregationOperators.(Value) -> Value, + ): Value> = + of(this).filter(limit, variableName, predicate) + + /** + * Selects a subset of an array to return based on the specified [predicate], similarly to [Kotlin's `filter`][kotlin.collections.filter]. + * + * The returned elements are in the original order. + * + * ### Example + * + * ```kotlin + * class Sensor( + * val measurements: List, + * ) + * + * collection.updateManyWithPipeline { + * set { + * Sensor::measurements set (Sensor::measurements).filter { it gte of(0) } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/) + * + * @param limit If set, specifies a maximum number of elements returned: + * only the first [limit] matching elements are returned, even if there are more matching elements. + * Must be greater or equal to `1`, or be `null`. + * + * @param variableName The name of the temporary variable passed to the [predicate] lambda, which represents the + * current element being iterated over. By default, `"this"`. Setting this parameter is only useful when using + * nested [filter] or other similar calls, which could otherwise conflict. + */ + @KtMongoDsl + fun KProperty1>.filter( + limit: Value? = null, + variableName: String = "this", + predicate: AggregationOperators.(Value) -> Value, + ): Value> = + of(this).filter(limit, variableName, predicate) + + /** + * Selects a subset of an array to return based on the specified [predicate], similarly to [Kotlin's `filter`][kotlin.collections.filter]. + * + * The returned elements are in the original order. + * + * ### Example + * + * ```kotlin + * class Sensor( + * val measurements: List, + * ) + * + * collection.updateManyWithPipeline { + * set { + * Sensor::measurements set (Sensor::measurements).filter { it gte of(0) } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/) + * + * @param limit If set, specifies a maximum number of elements returned: + * only the first [limit] matching elements are returned, even if there are more matching elements. + * Must be greater or equal to `1`, or be `null`. + * + * @param variableName The name of the temporary variable passed to the [predicate] lambda, which represents the + * current element being iterated over. By default, `"this"`. Setting this parameter is only useful when using + * nested [filter] or other similar calls, which could otherwise conflict. + */ + @KtMongoDsl + fun Collection.filter( + limit: Value? = null, + variableName: String = "this", + predicate: AggregationOperators.(Value) -> Value, + ): Value> = + of(this).filter(limit, variableName, predicate) + + @LowLevelApi + private class PredicateEvaluator(override val context: BsonContext) : AggregationOperators + + @LowLevelApi + private class ThisValue( + private val variableName: String, + context: BsonContext, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeString("$$$variableName") + } + } + + @LowLevelApi + private class FilterValueOperator( + private val input: Value>, + private val predicate: Value, + private val variableName: String, + private val limit: Value?, + context: BsonContext, + ) : AbstractValue>(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeDocument("\$filter") { + write("input") { + input.writeTo(this) + } + + writeString("as", variableName) + + write("cond") { + predicate.writeTo(this) + } + + if (limit != null) { + write("limit") { + limit.writeTo(this) + } + } + } + } + } + } + + // endregion + // region $firstN + + /** + * Returns the first [limit] elements in an array, similar to [kotlin.collections.take]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val firstScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::firstScores set Player::scores.take(3) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#array-operator) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value>.take( + limit: Value, + ): Value> = + TakeValueOperator( + input = this, + limit = limit, + context = context, + ) + + /** + * Returns the first [limit] elements in an array, similar to [kotlin.collections.take]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val firstScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::firstScores set Player::scores.take(3) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#array-operator) + */ + @KtMongoDsl + fun Field>.take( + limit: Value, + ): Value> = + of(this).take(limit) + + /** + * Returns the first [limit] elements in an array, similar to [kotlin.collections.take]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val firstScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::firstScores set Player::scores.take(3) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#array-operator) + */ + @KtMongoDsl + fun KProperty1>.take( + limit: Value, + ): Value> = + of(this).take(limit) + + /** + * Returns the first [limit] elements in an array, similar to [kotlin.collections.take]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val firstScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::firstScores set Player::scores.take(3) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#array-operator) + */ + @KtMongoDsl + fun Collection.take( + limit: Value, + ): Value> = + of(this).take(limit) + + @LowLevelApi + private class TakeValueOperator( + private val input: Value>, + private val limit: Value, + context: BsonContext, + ) : AbstractValue>(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeDocument("\$firstN") { + write("input") { + input.writeTo(this) + } + + write("n") { + limit.writeTo(this) + } + } + } + } + } + + // endregion + // region $lastN + + /** + * Returns the last [limit] elements in an array, similar to [kotlin.collections.takeLast]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val lastScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::lastScores set Player::scores.takeLast(3) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/#array-operator) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value>.takeLast( + limit: Value, + ): Value> = + TakeLastValueOperator( + input = this, + limit = limit, + context = context, + ) + + /** + * Returns the last [limit] elements in an array, similar to [kotlin.collections.takeLast]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val lastScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::lastScores set Player::scores.takeLast(3) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/#array-operator) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Field>.takeLast( + limit: Value, + ): Value> = + of(this).takeLast(limit) + + /** + * Returns the last [limit] elements in an array, similar to [kotlin.collections.takeLast]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val lastScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::lastScores set Player::scores.takeLast(3) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/#array-operator) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun KProperty1>.takeLast( + limit: Value, + ): Value> = + of(this).takeLast(limit) + + /** + * Returns the last [limit] elements in an array, similar to [kotlin.collections.takeLast]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * val lastScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::lastScores set Player::scores.takeLast(3) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/#array-operator) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Collection.takeLast( + limit: Value, + ): Value> = + of(this).takeLast(limit) + + @LowLevelApi + private class TakeLastValueOperator( + private val input: Value>, + private val limit: Value, + context: BsonContext, + ) : AbstractValue>(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeDocument("\$lastN") { + write("input") { + input.writeTo(this) + } + + write("n") { + limit.writeTo(this) + } + } + } + } + } + + // endregion + // region $map + + /** + * Applies a [transform] to all elements in an array and returns the array with the applied results, similar to + * [kotlin.collections.map]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::scores set Player::scores + * .map { + * it + of(1) + * } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value>.map( + variableName: String = "this", + transform: AggregationOperators.(Value) -> Value, + ): Value> = + MapValueOperator( + input = this, + transform = PredicateEvaluator(context).transform(ThisValue(variableName, context)), + variableName = variableName, + context = context, + ) + + /** + * Applies a [transform] to all elements in an array and returns the array with the applied results, similar to + * [kotlin.collections.map]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::scores set Player::scores + * .map { + * it + of(1) + * } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/) + */ + @KtMongoDsl + fun Field>.map( + variableName: String = "this", + transform: AggregationOperators.(Value) -> Value, + ): Value> = + of(this).map(variableName, transform) + + /** + * Applies a [transform] to all elements in an array and returns the array with the applied results, similar to + * [kotlin.collections.map]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::scores set Player::scores + * .map { + * it + of(1) + * } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/) + */ + @KtMongoDsl + fun KProperty1>.map( + variableName: String = "this", + transform: AggregationOperators.(Value) -> Value, + ): Value> = + of(this).map(variableName, transform) + + /** + * Applies a [transform] to all elements in an array and returns the array with the applied results, similar to + * [kotlin.collections.map]. + * + * ### Example + * + * ```kotlin + * class Player( + * val _id: ObjectId, + * val scores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::scores set Player::scores + * .map { + * it + of(1) + * } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/) + */ + @KtMongoDsl + fun Collection.map( + variableName: String = "this", + transform: AggregationOperators.(Value) -> Value, + ): Value> = + of(this).map(variableName, transform) + + @LowLevelApi + private class MapValueOperator( + private val input: Value<*, *>, + private val transform: Value<*, *>, + private val variableName: String, + context: BsonContext, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeDocument("\$map") { + write("input") { + input.writeTo(this) + } + + writeString("as", variableName) + + write("in") { + transform.writeTo(this) + } + } + } + } + } + + // endregion + // region $sortArray + + /** + * Sorts an array based on fields of its elements. + * + * ### Example + * + * ```kotlin + * class Score( + * val value: Int, + * ) + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val bestScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sortedBy { ascending(Score::value) } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sorted Sort by the elements themselves (ascending order). + * @see sortedDescending Sort by the elements themselves (descending order). + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value>.sortedBy( + order: SortOptionDsl.() -> Unit, + ): Value> = + SortValueOperator( + input = this, + sortOrder = SortOptionDslBsonNode(context).apply { order() }.toValue(), + context = context, + ) + + /** + * Sorts an array based on fields of its elements. + * + * ### Example + * + * ```kotlin + * class Score( + * val value: Int, + * ) + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val bestScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sortedBy { ascending(Score::value) } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sorted Sort by the elements themselves (ascending order). + * @see sortedDescending Sort by the elements themselves (descending order). + */ + @KtMongoDsl + fun Field>.sortedBy( + order: SortOptionDsl.() -> Unit, + ): Value> = + of(this).sortedBy(order) + + /** + * Sorts an array based on fields of its elements. + * + * ### Example + * + * ```kotlin + * class Score( + * val value: Int, + * ) + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val bestScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sortedBy { ascending(Score::value) } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sorted Sort by the elements themselves (ascending order). + * @see sortedDescending Sort by the elements themselves (descending order). + */ + @KtMongoDsl + fun KProperty1>.sortedBy( + order: SortOptionDsl.() -> Unit, + ): Value> = + of(this).sortedBy(order) + + /** + * Sorts an array based on fields of its elements. + * + * ### Example + * + * ```kotlin + * class Score( + * val value: Int, + * ) + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val bestScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sortedBy { ascending(Score::value) } + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sorted Sort by the elements themselves (ascending order). + * @see sortedDescending Sort by the elements themselves (descending order). + */ + @KtMongoDsl + fun Collection.sortedBy( + order: SortOptionDsl.() -> Unit, + ): Value> = + of(this).sortedBy(order) + + @LowLevelApi + private class SortOptionDslBsonNode( + context: BsonContext, + ) : AbstractCompoundBsonNode(context), SortOptionDsl { + + @OptIn(DangerousMongoApi::class) + override fun ascending(field: Field) { + accept(SortBsonNode(field.path, 1, context)) + } + + @OptIn(DangerousMongoApi::class) + override fun descending(field: Field) { + accept(SortBsonNode(field.path, -1, context)) + } + + @LowLevelApi + private class SortBsonNode( + val path: Path, + val value: Int, + context: BsonContext, + ) : AbstractBsonNode(context) { + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeInt32(path.toString(), value) + } + } + + fun toValue() = SortOptionDslValue(context) + + @LowLevelApi + private inner class SortOptionDslValue( + context: BsonContext, + ) : AbstractValue(context) { + + init { + this@SortOptionDslBsonNode.freeze() + } + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + this@SortOptionDslBsonNode.writeTo(this) + } + } + } + } + + /** + * Sorts an array based on its elements, in ascending order. + * + * ### Example + * + * ```kotlin + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val worstScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sorted() + * .take(5) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sortedBy Sort by fields of elements. + * @see sortedDescending Sort by elements in descending order. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value>.sorted(): Value> = + SortValueOperator( + input = this, + sortOrder = SortSelfValueOperator(order = 1, context), + context = context, + ) + + /** + * Sorts an array based on its elements, in ascending order. + * + * ### Example + * + * ```kotlin + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val worstScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sorted() + * .take(5) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sortedBy Sort by fields of elements. + * @see sortedDescending Sort by elements in descending order. + */ + @KtMongoDsl + fun Field>.sorted(): Value> = + of(this).sorted() + + /** + * Sorts an array based on its elements, in ascending order. + * + * ### Example + * + * ```kotlin + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val worstScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sorted() + * .take(5) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sortedBy Sort by fields of elements. + * @see sortedDescending Sort by elements in descending order. + */ + @KtMongoDsl + fun KProperty1>.sorted(): Value> = + of(this).sorted() + + /** + * Sorts an array based on its elements, in ascending order. + * + * ### Example + * + * ```kotlin + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val worstScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sorted() + * .take(5) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sortedBy Sort by fields of elements. + * @see sortedDescending Sort by elements in descending order. + */ + @KtMongoDsl + fun Collection.sorted(): Value> = + of(this).sorted() + + /** + * Sorts an array based on its elements, in descending order. + * + * ### Example + * + * ```kotlin + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val bestScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sortedDescending() + * .take(5) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sortedBy Sort by fields of elements. + * @see sortedDescending Sort by fields of elements. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value>.sortedDescending(): Value> = + SortValueOperator( + input = this, + sortOrder = SortSelfValueOperator(order = -1, context), + context = context, + ) + + /** + * Sorts an array based on its elements, in descending order. + * + * ### Example + * + * ```kotlin + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val bestScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sortedDescending() + * .take(5) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sortedBy Sort by fields of elements. + * @see sortedDescending Sort by fields of elements. + */ + @KtMongoDsl + fun Field>.sortedDescending(): Value> = + of(this).sortedDescending() + + /** + * Sorts an array based on its elements, in descending order. + * + * ### Example + * + * ```kotlin + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val bestScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sortedDescending() + * .take(5) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sortedBy Sort by fields of elements. + * @see sortedDescending Sort by fields of elements. + */ + @KtMongoDsl + fun KProperty1>.sortedDescending(): Value> = + of(this).sortedDescending() + + /** + * Sorts an array based on its elements, in descending order. + * + * ### Example + * + * ```kotlin + * + * class Player( + * val _id: ObjectId, + * val scores: List, + * val bestScores: List, + * ) + * + * players.updateManyWithPipeline { + * set { + * Player::bestScores set Player::scores + * .sortedDescending() + * .take(5) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/) + * + * @see sortedBy Sort by fields of elements. + * @see sortedDescending Sort by fields of elements. + */ + @KtMongoDsl + fun Collection.sortedDescending(): Value> = + of(this).sortedDescending() + + @LowLevelApi + private class SortSelfValueOperator( + private val order: Int, + context: BsonContext, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeInt32(order) + } + } + + @LowLevelApi + private class SortValueOperator( + private val input: Value>, + private val sortOrder: Value, + context: BsonContext, + ) : AbstractValue>(context) { + + init { + sortOrder.freeze() + } + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeDocument("\$sortArray") { + write("input") { + input.writeTo(this) + } + + write("sortBy") { + sortOrder.writeTo(this) + } + } + } + } + } + + // endregion + +} diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ComparisonValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ComparisonValueOperators.kt new file mode 100644 index 00000000..319485aa --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ComparisonValueOperators.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/operators/ComparisonValueOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.operators + +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AbstractValue +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Value +import opensavvy.ktmongo.dsl.query.FilterQuery + +/** + * Operators to compare two values. + * + * To learn more about aggregation operators, view [AggregationOperators]. + */ +interface ComparisonValueOperators : ValueOperators { + + /** + * Compares two aggregation values and returns `true` if they are equivalent. + * + * ### Example + * + * ```kotlin + * class Product( + * val name: String, + * val creationDate: Instant, + * val releaseDate: Instant, + * ) + * + * val releasedOnCreation = collection.aggregate() + * .match { + * expr { + * of(Product::creationDate) eq of(Product::releaseDate) + * } + * } + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/eq/) + * - [Comparison algorithm](https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order/#std-label-bson-types-comparison-order) + * + * @see ne Negation of this operator. + * @see FilterQuery.eq Equivalent operator in regular queries. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + infix fun Value.eq(other: Value): Value = + ComparisonValueOperator(context, this, other, "eq") + + /** + * Compares two aggregation values and returns `true` if they are not equivalent. + * + * ### Example + * + * ```kotlin + * class Product( + * val name: String, + * val creationDate: Instant, + * val releaseDate: Instant, + * ) + * + * val notReleasedOnCreation = collection.aggregate() + * .match { + * expr { + * of(Product::creationDate) eq of(Product::releaseDate) + * } + * } + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/ne/) + * - [Comparison algorithm](https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order/#std-label-bson-types-comparison-order) + * + * @see eq Negation of this operator. + * @see FilterQuery.ne Equivalent operator in regular queries. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + infix fun Value.ne(other: Value): Value = + ComparisonValueOperator(context, this, other, "ne") + + // TODO: document the other operators once 'project' is implemented, since the official examples use 'project' to demonstrate them + + @OptIn(LowLevelApi::class) + @KtMongoDsl + infix fun Value.gt(other: Value): Value = + ComparisonValueOperator(context, this, other, "gt") + + @OptIn(LowLevelApi::class) + @KtMongoDsl + infix fun Value.gte(other: Value): Value = + ComparisonValueOperator(context, this, other, "gte") + + @OptIn(LowLevelApi::class) + @KtMongoDsl + infix fun Value.lt(other: Value): Value = + ComparisonValueOperator(context, this, other, "lt") + + @OptIn(LowLevelApi::class) + @KtMongoDsl + infix fun Value.lte(other: Value): Value = + ComparisonValueOperator(context, this, other, "lte") + + @OptIn(LowLevelApi::class) + private class ComparisonValueOperator( + context: BsonContext, + private val operandA: Value, + private val operandB: Value, + private val operator: String, + ) : AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("$$operator") { + operandA.writeTo(this) + operandB.writeTo(this) + } + } + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ConditionalValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ConditionalValueOperators.kt new file mode 100644 index 00000000..cebe4041 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ConditionalValueOperators.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/operators/ConditionalValueOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.operators + +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AbstractValue +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Value + +/** + * Operators to conditionally create a value. + * + * To learn more about aggregation operators, view [AggregationOperators]. + */ +@KtMongoDsl +interface ConditionalValueOperators : ValueOperators { + + /** + * Decides between two [values][Value] depending on the evaluation of a boolean value. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val score: Int, + * val multiplier: Int, + * ) + * + * users.updateManyWithPipeline { + * set { + * User::score set cond( + * condition = of(User::multiplier) gt of(2), + * ifTrue = of(User::score) * of(User::multiplier), + * ifFalse = of(User::score) + * ) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + * + * @see switch Specify multiple conditions. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun cond( + condition: Value, + ifTrue: Value, + ifFalse: Value, + ): Value = + ConditionalValue(context, condition, ifTrue, ifFalse) + + @OptIn(LowLevelApi::class) + private class ConditionalValue( + context: BsonContext, + private val condition: Value, + private val ifTrue: Value, + private val ifFalse: Value, + ) : Value, AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeDocument("\$cond") { + write("if") { + condition.writeTo(this) + } + + write("then") { + ifTrue.writeTo(this) + } + + write("else") { + ifFalse.writeTo(this) + } + } + } + } + } + + /** + * Selects one value based on multiple conditions. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val score: Int, + * val role: String, + * val bonus: Int?, + * ) + * + * users.updateManyWithPipeline { + * set { + * User::bonus set switch( + * of(User::role) eq of("GUEST") to of(5), + * of(User::role) eq of("EMPLOYEE") to of(6), + * of(User::role) eq of("ADMIN") to of(7), + * default = of(-1) + * ) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/switch/) + * + * @see cond Specify a single condition. + */ + @KtMongoDsl + @OptIn(LowLevelApi::class) + fun switch( + vararg cases: Pair, Value>, + default: Value? = null, + ): Value = + SwitchValue(context, cases.asList(), default) + + @OptIn(LowLevelApi::class) + private class SwitchValue( + context: BsonContext, + private val cases: List, Value>>, + private val default: Value?, + ) : Value, AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeDocument("\$switch") { + writeArray("branches") { + for ((condition, value) in cases) { + writeDocument { + write("case") { + condition.writeTo(this) + } + + write("then") { + value.writeTo(this) + } + } + } + } + + if (default != null) { + write("default") { + default.writeTo(this) + } + } + } + } + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/StringValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/StringValueOperators.kt new file mode 100644 index 00000000..cca9ac1d --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/operators/StringValueOperators.kt @@ -0,0 +1,1129 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/operators/StringValueOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.operators + +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AbstractValue +import opensavvy.ktmongo.dsl.aggregation.Value + +/** + * String aggregation operators. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/#string-expression-operators) + */ +@KtMongoDsl +interface StringValueOperators : ValueOperators { + + // region $trim + + /** + * Removes whitespace characters, including null, or the specified characters from the beginning and end of a string. + * + * By default, removes whitespace characters including the null character. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trim() + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/trim/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.trim(): Value = + TrimValueOperator(context, this, null, trimStart = true, trimEnd = true) + + /** + * Removes the specified [characters] from the beginning and end of a string. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * // Trim both 'g' and 'e' characters from the beginning and end + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trim('g', 'e') + * }.toList() + * + * // Trim space, 'g', and 'e' characters from the beginning and end + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trim(' ', 'g', 'e') + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/trim/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.trim(vararg characters: Char): Value = + TrimValueOperator(context, this, of(characters.joinToString(separator = "")), trimStart = true, trimEnd = true) + + /** + * Removes the specified [characters] from the beginning and end of a string. + * + * The [characters] parameter is a single string that can contain multiple characters to be trimmed. + * Each character in the string will be removed from both the beginning and end of the input string. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * // Trim both 'g' and 'e' characters from the beginning and end + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trim(chars = of("ge")) + * }.toList() + * + * // Trim space, 'g', and 'e' characters from the beginning and end + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trim(chars = of(" ge")) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/trim/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.trim(characters: Value): Value = + TrimValueOperator(context, this, characters, trimStart = true, trimEnd = true) + + // endregion + // region $ltrim + + /** + * Removes whitespace characters, including null, or the specified characters from the beginning of a string. + * + * By default, removes whitespace characters including the null character. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimStart() + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/ltrim/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.trimStart(): Value = + TrimValueOperator(context, this, null, trimStart = true, trimEnd = false) + + /** + * Removes the specified [characters] from the beginning of a string. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * // Trim both 'g' and 'e' characters from the beginning + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimStart('g', 'e') + * }.toList() + * + * // Trim space, 'g', and 'e' characters from the beginning + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimStart(' ', 'g', 'e') + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/ltrim/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.trimStart(vararg characters: Char): Value = + TrimValueOperator(context, this, of(characters.joinToString(separator = "")), trimStart = true, trimEnd = false) + + /** + * Removes the specified [characters] from the beginning of a string. + * + * The [characters] parameter is a single string that can contain multiple characters to be trimmed. + * Each character in the string will be removed from the beginning of the input string. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * // Trim both 'g' and 'e' characters from the beginning + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimStart(characters = of("ge")) + * }.toList() + * + * // Trim space, 'g', and 'e' characters from the beginning + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimStart(characters = of(" ge")) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/ltrim/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.trimStart(characters: Value): Value = + TrimValueOperator(context, this, characters, trimStart = true, trimEnd = false) + + // endregion + // region $rtrim + + /** + * Removes whitespace characters, including null, or the specified characters from the end of a string. + * + * By default, removes whitespace characters including the null character. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimEnd() + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/rtrim/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.trimEnd(): Value = + TrimValueOperator(context, this, null, trimStart = false, trimEnd = true) + + /** + * Removes the specified [characters] from the end of a string. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * // Trim both 'g' and 'e' characters from the end + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimEnd('g', 'e') + * }.toList() + * + * // Trim space, 'g', and 'e' characters from the end + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimEnd(' ', 'g', 'e') + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/rtrim/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.trimEnd(vararg characters: Char): Value = + TrimValueOperator(context, this, of(characters.joinToString(separator = "")), trimStart = false, trimEnd = true) + + /** + * Removes the specified [characters] from the end of a string. + * + * The [characters] parameter is a single string that can contain multiple characters to be trimmed. + * Each character in the string will be removed from the end of the input string. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * // Trim both 'g' and 'e' characters from the end + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimEnd(characters = of("ge")) + * }.toList() + * + * // Trim space, 'g', and 'e' characters from the end + * collection.aggregate() + * .set { + * Document::text set of(Document::text).trimEnd(characters = of(" ge")) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/rtrim/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.trimEnd(characters: Value): Value = + TrimValueOperator(context, this, characters, trimStart = false, trimEnd = true) + + // endregion + // region $toLower + + /** + * Converts a string to lowercase, returning the result. + * + * If the argument resolves to `null`, `$toLower` returns an empty string `""`. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * collection.aggregate() + * .set { + * Document::text set of(Document::text).lowercase() + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLower/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.lowercase(): Value = + UnaryStringValueOperator(context, "toLower", this) + + // endregion + // region $toUpper + + /** + * Converts a string to uppercase, returning the result. + * + * If the argument resolves to `null`, `$toUpper` returns an empty string `""`. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * collection.aggregate() + * .set { + * Document::text set of(Document::text).uppercase() + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/toUpper/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.uppercase(): Value = + UnaryStringValueOperator(context, "toUpper", this) + + // endregion + // region $strLenCP + + /** + * Returns the number of code points in the specified string. + * + * If the argument resolves to `null`, this function returns `null`. + * + * ### Counting characters + * + * This function uses MongoDB's `$strLenCP` operator, which counts characters using Unicode code points. + * This differs from Kotlin's [String.length], which uses UTF-16 code units. + * For strings containing characters outside the Basic Multilingual Plane (like emoji or certain mathematical symbols), + * the counting behavior will differ. + * + * For example, the emoji "👨‍👩‍👧‍👦" (family) is a single Unicode grapheme cluster but consists of multiple code points. + * According to this operator, it has a length of 7. + * However, according to Kotlin's [String.length], it has a length of 11. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * val length: Int, + * ) + * + * collection.aggregate() + * .set { + * Document::length set of(Document::text).length + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenCP/) + * + * @see lengthUTF8 + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + val Value.length: Value + get() = StrLenCPValueOperator(context, this) + + // endregion + // region $strLenBytes + + /** + * Returns the number of UTF-8 encoded bytes in the specified string. + * + * If the argument resolves to `null`, this function returns `null`. + * + * ### Counting characters + * + * This function uses MongoDB's `$strLenBytes` operator, which counts characters using UTF-8 encoded bytes where + * each code point, or character, may use between one and four bytes to encode. + * This differs from the [length] property which uses Unicode code points. + * + * For example, US-ASCII characters are encoded using one byte. + * Characters with diacritic markings and additional Latin alphabetical characters are encoded using two bytes. + * Chinese, Japanese and Korean characters typically require three bytes, and other planes of Unicode + * (emoji, mathematical symbols, etc.) require four bytes. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * val byteLength: Int, + * ) + * + * collection.aggregate() + * .set { + * Document::byteLength set of(Document::text).lengthUTF8 + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenBytes/) + * + * @see length + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + val Value.lengthUTF8: Value + get() = StrLenBytesValueOperator(context, this) + + // endregion + // region $substrCP + + /** + * Returns the substring of a string. + * + * The substring starts with the character at the specified Unicode code point [startIndex] (zero-based) in the string and continues for the [length] number of code points specified. + * + * Note that this behavior is different from [String.substring], which expects start and end indexes. + * + * ### Counting characters + * + * This function uses MongoDB's `$substrCP` operator, which counts characters using Unicode code points. + * This differs from Kotlin's [String.substring], which uses UTF-16 code units. + * For strings containing characters outside the Basic Multilingual Plane (like emoji or certain mathematical symbols), + * the indexing behavior will differ. + * + * For example, the emoji "👨‍👩‍👧‍👦" (family) is a single Unicode grapheme cluster but consists of multiple code points. + * According to this operator, it has a size of 5. + * However, according to Kotlin's [String.substring], it has a size of 11. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * collection.aggregate() + * .set { + * Document::text set of(Document::text).substring(startIndex = of(1), length = of(2)) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/) + * + * @see substringUTF8 + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.substring(startIndex: Value, length: Value): Value = + SubstrCPValueOperator(context, this, startIndex, length) + + /** + * Returns the substring of a string. + * + * The substring contains the Unicode code points that are contained within [indexes]. + * + * ### Counting characters + * + * This function uses MongoDB's `$substrCP` operator, which counts characters using Unicode code points. + * This differs from Kotlin's [String.substring], which uses UTF-16 code units. + * For strings containing characters outside the Basic Multilingual Plane (like emoji or certain mathematical symbols), + * the indexing behavior will differ. + * + * For example, the emoji "👨‍👩‍👧‍👦" (family) is a single Unicode grapheme cluster but consists of multiple code points. + * According to this operator, it has a size of 5. + * However, according to Kotlin's [String.substring], it has a size of 11. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * collection.aggregate() + * .set { + * Document::text set of(Document::text).substring(1..2) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/) + * + * @see substringUTF8 + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.substring(indexes: IntRange): Value = + SubstrCPValueOperator(context, this, of(indexes.first), of(indexes.last - indexes.first)) + + // endregion + // region $substrBytes + + /** + * Returns the substring of a string. + * + * The substring starts with the character at the specified UTF-8 byte [startIndex] in the string and continues for the [byteCount] number of bytes. + * + * Note that this behavior is different from [String.substring], which expects start and end indexes. + * + * ### Counting characters + * + * This function uses MongoDB's `$substrBytes` operator, which counts characters using UTF-8 encoded bytes where + * each code point, or character, may use between one and four bytes to encode. + * This differs from the [substring] function which uses Unicode code points. + * + * For example, US-ASCII characters are encoded using one byte. + * Characters with diacritic markings and additional Latin alphabetical characters are encoded using two bytes. + * Chinese, Japanese and Korean characters typically require three bytes, and other planes of Unicode + * (emoji, mathematical symbols, etc.) require four bytes. + * + * If [startIndex] or [byteCount] happen to be within a multibyte character, an error will be thrown. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * collection.aggregate() + * .set { + * Document::text set of(Document::text).substringUTF8(startIndex = of(1), byteCount = of(2)) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/) + * + * @see substring + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.substringUTF8(startIndex: Value, byteCount: Value): Value = + SubstrBytesValueOperator(context, this, startIndex, byteCount) + + /** + * Returns the substring of a string. + * + * The substring contains the Unicode code points that are contained within [indexes]. + * + * ### Counting characters + * + * This function uses MongoDB's `$substrBytes` operator, which counts characters using UTF-8 encoded bytes where + * each code point, or character, may use between one and four bytes to encode. + * This differs from the [substring] function which uses Unicode code points. + * + * For example, US-ASCII characters are encoded using one byte. + * Characters with diacritic markings and additional Latin alphabetical characters are encoded using two bytes. + * Chinese, Japanese and Korean characters typically require three bytes, and other planes of Unicode + * (emoji, mathematical symbols, etc.) require four bytes. + * + * If the start or end index happens to be within a multibyte character, an error will be thrown. + * + * ### Example + * + * ```kotlin + * class Document( + * val text: String, + * ) + * + * collection.aggregate() + * .set { + * Document::text set of(Document::text).substringUTF8(1..2) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/) + * + * @see substring + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.substringUTF8(indexes: IntRange): Value = + SubstrBytesValueOperator(context, this, of(indexes.first), of(indexes.last - indexes.first)) + + // endregion + // region $split + + /** + * Divides a string into an array of substrings based on a [delimiter]. + * + * `$split` removes the delimiter and returns the resulting substrings as elements of an array. + * If the delimiter is not found in the string, `$split` returns the original string as the only element of an array. + * + * Both the string expression and delimiter must be strings. Otherwise, the operation fails with an error. + * + * ### Example + * + * ```kotlin + * class Document( + * val city: String, + * val cityState: List, + * ) + * + * collection.aggregate() + * .set { + * Document::cityState set of(Document::city).split(of(", ")) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/split/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.split(delimiter: Value): Value?> = + SplitValueOperator(context, this, delimiter) + + /** + * Divides a string into an array of substrings based on a [delimiter]. + * + * `$split` removes the delimiter and returns the resulting substrings as elements of an array. + * If the delimiter is not found in the string, `$split` returns the original string as the only element of an array. + * + * The string expression must be a string. Otherwise, the operation fails with an error. + * + * ### Example + * + * ```kotlin + * class Document( + * val city: String, + * val cityState: List, + * ) + * + * collection.aggregate() + * .set { + * Document::cityState set of(Document::city).split(", ") + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/split/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.split(delimiter: String): Value?> = + SplitValueOperator(context, this, of(delimiter)) + + // endregion + // region $replaceOne + + /** + * Replaces the first instance of [find] with a [replacement] string. + * + * If no occurrences of [find] are found in the input string, the input string is returned. + * If any of the input, [find] or [replacement] is `null`, `null` is returned. + * + * The input, [find], and [replacement] expressions must evaluate to a string or a `null`, or `$replaceOne` fails with an error. + * + * ### Example + * + * ```kotlin + * class Document( + * val item: String, + * ) + * + * collection.aggregate() + * .set { + * Document::item set of(Document::item).replaceFirst(find = of("blue paint"), replacement = of("red paint")) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceOne/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.replaceFirst(find: Value, replacement: Value): Value = + ReplaceValueOperator(context, "\$replaceOne", this, find, replacement) + + /** + * Replaces the first instance of [find] with a [replacement] string. + * + * If no occurrences of [find] are found in the input string, the input string is returned. + * If the input is `null`, `null` is returned. + * + * The input must evaluate to a string or a `null`, or `$replaceOne` fails with an error. + * + * ### Example + * + * ```kotlin + * class Document( + * val item: String, + * ) + * + * collection.aggregate() + * .set { + * Document::item set of(Document::item).replaceFirst(find = "blue paint", replacement = "red paint") + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceOne/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.replaceFirst(find: String, replacement: String): Value = + ReplaceValueOperator(context, "\$replaceOne", this, of(find), of(replacement)) + + // endregion + // region $replaceAll + + /** + * Replaces all instances of [find] with a [replacement] string. + * + * If no occurrences of [find] are found in the input string, the input string is returned. + * If any of the input, [find] or [replacement] is `null`, `null` is returned. + * + * The input, [find], and [replacement] expressions must evaluate to a string or a `null`, or `$replaceAll` fails with an error. + * + * ### Example + * + * ```kotlin + * class Document( + * val item: String, + * ) + * + * collection.aggregate() + * .set { + * Document::item set of(Document::item).replace(find = of("blue paint"), replacement = of("red paint")) + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceAll/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.replace(find: Value, replacement: Value): Value = + ReplaceValueOperator(context, "\$replaceAll", this, find, replacement) + + /** + * Replaces all instances of [find] with a [replacement] string. + * + * If no occurrences of [find] are found in the input string, the input string is returned. + * If the input is `null`, `null` is returned. + * + * The input must evaluate to a string or a `null`, or `$replaceAll` fails with an error. + * + * ### Example + * + * ```kotlin + * class Document( + * val item: String, + * ) + * + * collection.aggregate() + * .set { + * Document::item set of(Document::item).replace(find = "blue paint", replacement = "red paint") + * }.toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceAll/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.replace(find: String, replacement: String): Value = + ReplaceValueOperator(context, "\$replaceAll", this, of(find), of(replacement)) + + // endregion + + @LowLevelApi + private class UnaryStringValueOperator( + context: BsonContext, + private val operator: String, + private val value: Value, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + write("$$operator") { + value.writeTo(this) + } + } + } + } + + @LowLevelApi + private class TrimValueOperator( + context: BsonContext, + private val input: Value, + private val chars: Value?, + private val trimStart: Boolean = true, + private val trimEnd: Boolean = true, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + val operator = when { + trimStart && trimEnd -> "\$trim" + trimStart -> "\$ltrim" + trimEnd -> "\$rtrim" + else -> throw IllegalArgumentException("At least one of trimStart or trimEnd must be true") + } + + writeDocument(operator) { + write("input") { + input.writeTo(this) + } + if (chars != null) { + write("chars") { + chars.writeTo(this) + } + } + } + } + } + } + + @LowLevelApi + private class SubstrCPValueOperator( + context: BsonContext, + private val input: Value, + private val startIndex: Value, + private val length: Value, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$substrCP") { + input.writeTo(this) + startIndex.writeTo(this) + length.writeTo(this) + } + } + } + } + + @LowLevelApi + private class SubstrBytesValueOperator( + context: BsonContext, + private val input: Value, + private val startIndex: Value, + private val byteCount: Value, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$substrBytes") { + input.writeTo(this) + startIndex.writeTo(this) + byteCount.writeTo(this) + } + } + } + } + + @LowLevelApi + private class StrLenCPValueOperator( + context: BsonContext, + private val input: Value, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + write("\$strLenCP") { + input.writeTo(this) + } + } + } + } + + @LowLevelApi + private class StrLenBytesValueOperator( + context: BsonContext, + private val input: Value, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + write("\$strLenBytes") { + input.writeTo(this) + } + } + } + } + + @LowLevelApi + private class SplitValueOperator( + context: BsonContext, + private val input: Value, + private val delimiter: Value, + ) : AbstractValue?>(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$split") { + input.writeTo(this) + delimiter.writeTo(this) + } + } + } + } + + @LowLevelApi + private class ReplaceValueOperator( + context: BsonContext, + private val operator: String, + private val input: Value, + private val find: Value, + private val replacement: Value, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeDocument(operator) { + write("input") { + input.writeTo(this) + } + write("find") { + find.writeTo(this) + } + write("replacement") { + replacement.writeTo(this) + } + } + } + } + } + + // region $concat + + /** + * Concatenates strings together. + * + * If any of strings are `null`, the concatenation returns `null`. + * + * ### Example + * + * ```kotlin + * class Document( + * val firstName: String, + * val lastName: String, + * val fullName: String, + * ) + * + * collection.aggregate() + * .set { + * Document::fullName set concat(of(Document::firstName), of(" "), of(Document::lastName)) + * } + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/concat/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun concat(strings: List>): Value = + ConcatValueOperator(context, strings) + + /** + * Concatenates strings together. + * + * If any of strings are `null`, the concatenation returns `null`. + * + * ### Example + * + * ```kotlin + * class Document( + * val firstName: String, + * val lastName: String, + * val fullName: String, + * ) + * + * collection.aggregate() + * .set { + * Document::fullName set concat(of(Document::firstName), of(" "), of(Document::lastName)) + * } + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/concat/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun concat(vararg strings: Value): Value = + concat(strings.asList()) + + /** + * Concatenates strings together. + * + * If any of strings are `null`, the concatenation returns `null`. + * + * ### Example + * + * ```kotlin + * class Document( + * val firstName: String, + * val lastName: String, + * val fullName: String, + * ) + * + * collection.aggregate() + * .set { + * Document::fullName set (of(Document::firstName) concat of(" ") concat of(Document::lastName)) + * } + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/concat/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + infix fun Value.concat(other: Value): Value = + concat(listOf(this, other)) + + @LowLevelApi + private class ConcatValueOperator( + context: BsonContext, + private val strings: List>, + ) : AbstractValue(context) { + + override fun simplify(): AbstractValue { + val flattenedOperands = ArrayList>() + + for (operand in strings) { + if (operand is ConcatValueOperator) { + flattenedOperands += operand.strings + } else { + flattenedOperands += operand + } + } + + return if (flattenedOperands != strings) { + ConcatValueOperator(context, flattenedOperands) + } else { + this + } + } + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$concat") { + for (str in strings) { + str.writeTo(this) + } + } + } + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/TrigonometryValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/TrigonometryValueOperators.kt new file mode 100644 index 00000000..6b862a46 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/operators/TrigonometryValueOperators.kt @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/operators/TrigonometryValueOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.operators + +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AbstractValue +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Value + +/** + * Operators to perform trigonometric and geometric operators. + * + * To learn more about aggregation operators, see [AggregationOperators]. + */ +interface TrigonometryValueOperators : ValueOperators { + + // region cos/sin/tan + + /** + * The inverse cosine (arc cosine) of a value, in radians. + * + * The value must be in the range `-1..1`. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Triangle( + * val name: String, + * val sideA: Double, + * val sideB: Double, + * val hypotenuse: Double, + * val angleA: Double, + * ) + * + * collection.aggregate() + * .set { + * Triangle::angleA set acos(of(Triangle::sideB) / of(Triangle::hypotenuse)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/acos/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun acos(value: Value): Value = + UnaryTrigonometryOperator(context, "acos", value) + + /** + * The inverse hyperbolic cosine (hyperbolic arc cosine) of a value, in radians. + * + * The value must be in the range `1..∞`. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Trigonometry( + * val name: String, + * val x: Double, + * val y: Double, + * ) + * + * collection.aggregate() + * .set { + * Trigonometry::y set acosh(of(Trigonometry::x)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/acosh/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun acosh(value: Value): Value = + UnaryTrigonometryOperator(context, "acosh", value) + + /** + * The cosine of a value that is measured in radians. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Triangle( + * val name: String, + * val sideA: Double, + * val sideB: Double, + * val hypotenuse: Double, + * val angleA: Double, + * ) + * + * collection.aggregate() + * .set { + * Triangle::sideB set (of(cos(Triangle::angleA) * of(Triangle::hypotenuse))) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cos/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun cos(value: Value): Value = + UnaryTrigonometryOperator(context, "cos", value) + + /** + * The hyperbolic cosine of a value that is measured in radians. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Trigonometry( + * val name: String, + * val angle: Double, + * val cosh: Double, + * ) + * + * collection.aggregate() + * .set { + * Trigonometry::cosh set cosh(of(Trigonometry::angle)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cosh/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun cosh(value: Value): Value = + UnaryTrigonometryOperator(context, "cosh", value) + + /** + * The inverse sine (arc sine) of a value, in radians. + * + * The value must be in the range `-1..1`. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Triangle( + * val name: String, + * val sideA: Double, + * val sideB: Double, + * val hypotenuse: Double, + * val angleA: Double, + * ) + * + * collection.aggregate() + * .set { + * Triangle::angleA set asin(of(Triangle::sideA) / of(Triangle::hypotenuse)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/asin/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun asin(value: Value): Value = + UnaryTrigonometryOperator(context, "asin", value) + + /** + * The inverse hyperbolic sine (hyperbolic arc sine) of a value, in radians. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Trigonometry( + * val name: String, + * val x: Double, + * val y: Double, + * ) + * + * collection.aggregate() + * .set { + * Trigonometry::y set asinh(of(Trigonometry::x)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/asinh/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun asinh(value: Value): Value = + UnaryTrigonometryOperator(context, "asinh", value) + + /** + * The sine of a value that is measured in radians. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Triangle( + * val name: String, + * val sideA: Double, + * val sideB: Double, + * val hypotenuse: Double, + * val angleA: Double, + * ) + * + * collection.aggregate() + * .set { + * Triangle::sideB set (sin(of(Trigonometry::angleA)) * of(Trigonometry::hypotenuse)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sin/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun sin(value: Value): Value = + UnaryTrigonometryOperator(context, "sin", value) + + /** + * The hyperbolic sine of a value that is measured in radians. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Trigonometry( + * val name: String, + * val x: Double, + * val y: Double, + * ) + * + * collection.aggregate() + * .set { + * Trigonometry::y set sinh(of(Trigonometry::x)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sinh/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun sinh(value: Value): Value = + UnaryTrigonometryOperator(context, "sinh", value) + + /** + * The inverse tangent (arc tangent) of a value, in radians. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Triangle( + * val name: String, + * val sideA: Double, + * val sideB: Double, + * val hypotenuse: Double, + * val angleA: Double, + * ) + * + * collection.aggregate() + * .set { + * Triangle::angleA set atan(of(Triangle::sideB) / of(Triangle::sideA)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/atan/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun atan(value: Value): Value = + UnaryTrigonometryOperator(context, "atan", value) + + /** + * The inverse hyperbolic tangent (hyperbolic arc tangent) of a value, in radians. + * + * The value must be in the range `-1..1`. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Trigonometry( + * val name: String, + * val x: Double, + * val y: Double, + * ) + * + * collection.aggregate() + * .set { + * Trigonometry::y set atanh(of(Trigonometry::x)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/atanh/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun atanh(value: Value): Value = + UnaryTrigonometryOperator(context, "atanh", value) + + /** + * The tangent of a value that is measured in radians. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Triangle( + * val name: String, + * val sideA: Double, + * val sideB: Double, + * val hypotenuse: Double, + * val angleA: Double, + * ) + * + * collection.aggregate() + * .set { + * Triangle::sideB set (tan(of(Trigonometry::angleA) * of(Trigonometry::sideA))) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/tan/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun tan(value: Value): Value = + UnaryTrigonometryOperator(context, "tan", value) + + /** + * The hyperbolic tangent of a value that is measured in radians. + * + * If the value is `null` or `NaN`, it is returned unchanged. + * + * ### Example + * + * ```kotlin + * class Trigonometry( + * val name: String, + * val x: Double, + * val y: Double, + * ) + * + * collection.aggregate() + * .set { + * Trigonometry::y set tanh(of(Trigonometry::x)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/tanh/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun tanh(value: Value): Value = + UnaryTrigonometryOperator(context, "tanh", value) + + // endregion + // region °/radians + + /** + * Converts an angle in degrees to an angle in radians. + * + * ### Example + * + * ```kotlin + * class Trigonometry( + * val angleADeg: Double, + * val angleARad: Double, + * ) + * + * collection.updateManyWithPipeline { + * set { + * Trigonometry::angleARad set of(Trigonometry::angleADeg).toRadians() + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/degreesToRadians/) + * + * @see toDegrees Opposite operation + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toRadians(): Value = + UnaryTrigonometryOperator(context, "degreesToRadians", this) + + /** + * Converts an angle in radians to an angle in degrees. + * + * ### Example + * + * ```kotlin + * class Trigonometry( + * val angleADeg: Double, + * val angleARad: Double, + * ) + * + * collection.updateManyWithPipeline { + * set { + * Trigonometry::angleADeg set of(Trigonometry::angleARad).toDegrees() + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/radiansToDegrees/) + * + * @see toRadians Opposite operation + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toDegrees(): Value = + UnaryTrigonometryOperator(context, "radiansToDegrees", this) + + // endregion + + @OptIn(LowLevelApi::class) + private class UnaryTrigonometryOperator( + context: BsonContext, + private val operatorName: String, + private val value: Value, + ) : AbstractValue(context) { + + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + write("$$operatorName") { + value.writeTo(this) + } + } + } + } + +} diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/TypeValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/TypeValueOperators.kt new file mode 100644 index 00000000..735adf3a --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/operators/TypeValueOperators.kt @@ -0,0 +1,552 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/operators/TypeValueOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.operators + +import opensavvy.ktmongo.bson.BsonType +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.bson.types.ObjectId +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AbstractValue +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Value +import kotlin.time.Instant +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +/** + * Operators to interact with type information. + * + * To learn more about aggregation operators, view [AggregationOperators]. + */ +@KtMongoDsl +interface TypeValueOperators : ValueOperators { + + /** + * Gets the [BsonType] of the current value. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.aggregate() + * .project { + * Field.unsafe("nameIsString") set (of(User::name).type eq of(BsonType.String)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/) + * + * @see BsonType List of possible types. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + val Value.type: Value + get() = TypeValue(context, this) + + @OptIn(LowLevelApi::class) + private class TypeValue( + context: BsonContext, + private val value: Value, + ) : Value, AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + write("\$type") { + value.writeTo(this) + } + } + } + } + + /** + * Determines if this value is an array. + * + * ### Example + * + * ```kotlin + * class User( + * val data: String, + * ) + * + * collection.aggregate() + * .project { + * Field.unsafe("dataIsArray") set of(User::data).isArray + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/isArray/) + * + * @see type Get a value's type. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + val Value.isArray: Value + get() = IsArrayValue(context, this) + + @LowLevelApi + private class IsArrayValue( + context: BsonContext, + private val value: Value, + ) : Value, AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeArray("\$isArray") { + value.writeTo(this) + } + } + } + } + + /** + * Determines if this value is a number. + * + * The following types are considered numbers: + * - [BsonType.Int32] + * - [BsonType.Int64] + * - [BsonType.Double] + * - [BsonType.Decimal128] + * + * ### Example + * + * ```kotlin + * class User( + * val data: String, + * ) + * + * collection.aggregate() + * .project { + * Field.unsafe("dataIsNumber") set of(User::data).isNumber + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/) + * + * @see type Get a value's type. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + val Value.isNumber: Value + get() = IsNumberValue(context, this) + + @LowLevelApi + private class IsNumberValue( + context: BsonContext, + private val value: Value, + ) : Value, AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + write("\$isNumber") { + value.writeTo(this) + } + } + } + } + + /** + * Converts this value to a [BsonType.Boolean]. + * + * ### Conversion algorithm + * + * [BsonType.Boolean] is returned as itself. + * + * Numeric types ([BsonType.Int32], [BsonType.Int64], [BsonType.Double] and [BsonType.Decimal128]) consider that + * 0 is `false` and all other values are `true`. + * + * [BsonType.Null] always returns `null`. + * + * All other types always return `true`. + * + * ### Example + * + * ```kotlin + * class User( + * val foo: String + * ) + * + * users.aggregate() + * .project { + * Field.unsafe("asBoolean") set of(User::foo).toBoolean() + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/toBool) + * + * @see type Get the value's type. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toBoolean(): Value = + ConvertToValue(context, this, "Bool") + + /** + * Converts this value to an [Instant] ([BsonType.Datetime]). + * + * ### Conversion algorithm + * + * [BsonType.Datetime] is returned as itself. + * + * [BsonType.Int64], [BsonType.Double] and [BsonType.Decimal128] are interpreted as a timestamp from the UNIX epoch + * in milliseconds: positive values happen after the epoch, negative values happen before the epoch. + * + * [BsonType.String] is parsed using the ISO timestamp formats, for example: + * - `"2018-03-20"` + * - `"2018-03-20T12:00:00Z"` + * - `"2018-03-20T12:00:00+0500"` + * + * [BsonType.ObjectId] and [BsonType.Timestamp] are represented by extracting their timestamp component + * (both have a precision of one second). + * + * [BsonType.Null] always returns `null`. + * + * Other types throw an exception. + * + * ### Example + * + * ```kotlin + * class User( + * val modificationDate: Instant + * ) + * + * // Update old data + * users.updateManyWithPipeline( + * filter = { + * User::modificationType { + * not { + * hasType(BsonType.String) + * } + * } + * } + * ) { + * set { + * User::name set of(User::name).toInstant() + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/todate/) + * + * @see type Get the value's type. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toInstant(): Value = + ConvertToValue(context, this, "Date") + + /** + * Converts this value to a [BsonType.Double]. + * + * ### Conversion algorithm + * + * [BsonType.Double] is returned as itself. + * + * [BsonType.Int32] and [BsonType.Int64] are extended to a double. + * + * [BsonType.Boolean] becomes 1 if `true` and 0 if `false`. + * + * [BsonType.Decimal128] is converted if it is within the possible range of a double value. + * Otherwise, an exception is thrown. + * + * [BsonType.String] is parsed as a double. + * Only base 10 numbers can be parsed. + * If the value is outside the range of a double, an exception is thrown. + * `"-5.5"` and `"123456"` are two valid examples. + * + * [BsonType.Datetime] returns the number of milliseconds since the epoch. + * + * [BsonType.Null] always returns `null`. + * + * Other types throw an exception. + * + * ### Example + * + * ```kotlin + * class Metric( + * val instant: Instant, + * val millis: Double + * ) + * + * users.aggregate() + * .set { + * User::millis set of(Metric::instant).toDouble() + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/todouble) + * + * @see type Get the value's type. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toDouble(): Value = + ConvertToValue(context, this, "Double") + + /** + * Converts this value to an [Int] ([BsonType.Int32]). + * + * ### Conversion algorithm + * + * [BsonType.Int32] is returned as itself. + * + * [BsonType.Int64] is converted to an Int if it fits into the range. + * Otherwise, an exception is thrown. + * + * [BsonType.Boolean] becomes 1 if `true` and 0 if `false`. + * + * [BsonType.Double] and [BsonType.Decimal128] are truncated to an integer value. + * If this value does not fall in the valid range for an int, an exception is thrown. + * + * [BsonType.String] is parsed as an int. + * Only base 10 numbers can be parsed. + * If the value is outside the range of a double, an exception is thrown. + * `"-5"` and `"123456"` are two valid examples. + * Floating-point numbers are not supported (use [toDouble]). + * + * [BsonType.Datetime] returns the number of milliseconds since the epoch. + * + * [BsonType.Null] always returns `null`. + * + * Other types throw an exception. + * + * ### Example + * + * ```kotlin + * class Product( + * val quantity: Double, + * ) + * + * users.aggregate() + * .set { + * Field.unsafe("quantity") set of(Product::quantity).toInt() + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/toint) + * + * @see type Get the value's type. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toInt(): Value = + ConvertToValue(context, this, "Int") + + /** + * Converts this value to an [Long] ([BsonType.Int64]). + * + * ### Conversion algorithm + * + * [BsonType.Int64] is returned as itself. + * + * [BsonType.Int32] is extended to a Long. + * + * [BsonType.Boolean] becomes 1 if `true` and 0 if `false`. + * + * [BsonType.Double] and [BsonType.Decimal128] are truncated to an integer value. + * If this value does not fall in the valid range for a long, an exception is thrown. + * + * [BsonType.String] is parsed as a double. + * Only base 10 numbers can be parsed. + * If the value is outside the range of a double, an exception is thrown. + * `"-5"` and `"123456"` are two valid examples. + * Floating-point numbers are not supported (use [toDouble]). + * + * [BsonType.Datetime] returns the number of milliseconds since the epoch. + * + * [BsonType.Null] always returns `null`. + * + * Other types throw an exception. + * + * ### Example + * + * ```kotlin + * class Product( + * val quantity: Double, + * ) + * + * users.aggregate() + * .set { + * Field.unsafe("quantity") set of(Product::quantity).toLong() + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/tolong) + * + * @see type Get the value's type. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toLong(): Value = + ConvertToValue(context, this, "Long") + + /** + * Converts this value to an [ObjectId]. + * + * ### Conversion algorithm + * + * [BsonType.ObjectId] is returned as itself. + * + * [BsonType.String] is parsed as an [ObjectId] as a 24-character hexadecimal representation, + * for example `"5ab9cbfa31c2ab715d42129e"`. + * + * [BsonType.Null] always returns `null`. + * + * Other types throw an exception. + * + * ### Example + * + * ```kotlin + * class Product( + * val _id: ObjectId, + * ) + * + * // Migrate ids which were accidentally written as strings: + * products.updateManyWithPipeline { + * _id set of(Product::_id).toObjectId() + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/toObjectId) + * + * @see type Get the value's type. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toObjectId(): Value = + ConvertToValue(context, this, "ObjectId") + + /** + * Converts this value to a [String]. + * + * Note: the MongoDB operator is called `toString`, but that name would be ambiguous in Kotlin because of [Any.toString]. + * + * ### Conversion algorithm + * + * [BsonType.String] is always returned as itself. + * + * [BsonType.Int32], [BsonType.Int64], [BsonType.Double], [BsonType.Decimal128], [BsonType.Boolean] and + * [BsonType.BinaryData] are converted to a string. + * + * [BsonType.ObjectId] returns its hexadecimal representation. + * + * [BsonType.Datetime] is formatted in ISO. + * + * [BsonType.Null] always returns `null`. + * + * ### Example + * + * ```kotlin + * class Product( + * val _id: ObjectId, + * val age: Double, + * ) + * + * // Migrate ids which were accidentally written as strings: + * products.updateManyWithPipeline { + * _id set of(Product::age).toText() + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/toString) + * + * @see type Get the value's type. + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toText(): Value = + ConvertToValue(context, this, "String") + + /** + * Converts a string value to a [Uuid] ([BsonType.BinaryData]). + * + * ### Example + * + * ```kotlin + * class User( + * val _id: ObjectId, + * val eventId: Uuid, + * ) + * + * // Convert old 'eventId' data which was incorrectly created as strings + * users.updateManyWithPipeline { + * set { + * User::eventId set of(User::eventId).toUuid() + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/toUUID/) + * + * @see type Get the value's type. + */ + @ExperimentalUuidApi + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value.toUuid(): Value = + ConvertToValue(context, this, "UUID") + + @LowLevelApi + private class ConvertToValue( + context: BsonContext, + private val value: Value, + private val typeName: String, + ) : Value, AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + write("\$to$typeName") { + value.writeTo(this) + } + } + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt new file mode 100644 index 00000000..1fe0ecca --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.operators + +import opensavvy.ktmongo.bson.BsonType +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AbstractValue +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Value +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.path.FieldDsl +import opensavvy.ktmongo.dsl.path.Path +import kotlin.reflect.KProperty1 + +/** + * Supertype for all interface operators describing operators on aggregation values. + * + * Most of the time, end-users will be using the subtype [AggregationOperators] instead of this interface. + */ +interface ValueOperators : FieldDsl { + + @LowLevelApi + override val context: BsonContext + + /** + * Refers to a [field] within an [aggregation value][AggregationOperators]. + * + * ### Example + * + * ```kotlin + * class Product( + * val acceptanceDate: Instant, + * val publishingDate: Instant, + * ) + * + * val publishedBeforeAcceptance = products.find { + * expr { + * of(Product::publishingDate) lt of(Product::acceptanceDate) + * } + * } + * ``` + */ + @OptIn(LowLevelApi::class) + fun of(field: Field): Value = + FieldValue(field, context) + + /** + * Refers to a [field] within an [aggregation value][AggregationOperators]. + * + * ### Example + * + * ```kotlin + * class Product( + * val acceptanceDate: Instant, + * val publishingDate: Instant, + * ) + * + * val publishedBeforeAcceptance = products.find { + * expr { + * of(Product::publishingDate) lt of(Product::acceptanceDate) + * } + * } + * ``` + */ + fun of(field: KProperty1): Value = + of(field.field) + + /** + * Refers to a Kotlin [value] within an [aggregation value][AggregationOperators]. + * + * ### Example + * + * ```kotlin + * class Product( + * val age: Int, + * ) + * + * val publishedBeforeAcceptance = products.find { + * expr { + * of(Product::age) lt of(15) + * } + * } + * ``` + */ + @OptIn(LowLevelApi::class) + fun of(value: Result): Value = + LiteralValue(value, context) + + /** + * Refers to a [BsonType] within an [aggregation value][AggregationOperators]. + * + * ### Example + * + * ```kotlin + * class Product( + * val age: Int, + * ) + * + * val publishedBeforeAcceptance = products.find { + * expr { + * of(Product::age).type eq of(BsonType.Int32) + * } + * } + * ``` + */ + @OptIn(LowLevelApi::class) + fun of(value: BsonType): Value = + BsonTypeValue(value, context) + + /** + * Refers to [field] as a nested field of the current value. + * + * ### Examples + * + * ```kotlin + * class User( + * val name: String, + * ) + * + * class Data( + * val users: List, + * val userNames: List, + * ) + * + * data.aggregate() + * .set { + * Data::userNames set Data::users.map { it / User::name } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/getField/) + */ + @OptIn(LowLevelApi::class) + operator fun Value.div(field: Field): Value = + GetFieldValue(this, field.path, context) + + /** + * Refers to [field] as a nested field of the current value. + * + * ### Examples + * + * ```kotlin + * class User( + * val name: String, + * ) + * + * class Data( + * val users: List, + * val userNames: List, + * ) + * + * data.aggregate() + * .set { + * Data::userNames set Data::users.map { it / User::name } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/getField/) + */ + operator fun Value.div(field: KProperty1): Value = + this / field.field + +} + +@OptIn(LowLevelApi::class) +private class FieldValue( + val field: Field, + context: BsonContext, +) : AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) { + writer.writeString("$$field") + } +} + +@OptIn(LowLevelApi::class) +private class LiteralValue( + val value: Any?, + context: BsonContext, +) : AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) { + writer.writeDocument { + writeObjectSafe("\$literal", value) + } + } +} + +@OptIn(LowLevelApi::class) +private class BsonTypeValue( + val type: BsonType, + context: BsonContext, +) : AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) { + writer.writeDocument { + writeInt32("\$literal", type.code) + } + } +} + +@OptIn(LowLevelApi::class) +private class GetFieldValue( + private val root: Value, + private val child: Path, + context: BsonContext, +) : AbstractValue(context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + writeDocument("\$getField") { + write("input") { + root.writeTo(this) + } + + writeString("field", child.toString()) + } + } + } + +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Count.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Count.kt new file mode 100644 index 00000000..51c9ab47 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Count.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Count.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.path.FieldDslImpl +import opensavvy.ktmongo.dsl.path.Path +import opensavvy.ktmongo.dsl.path.PathSegment +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import kotlin.reflect.KProperty1 + +/** + * Pipeline implementing the `$count` stage. + */ +@KtMongoDsl +interface HasCount : Pipeline { + + /** + * Counts how many elements exist in the pipeline and outputs a single document containing a single [field] containing + * the count. + * + * [field] must be a simple field name. Subfields (accessed through the `/` operator) are forbidden. + * + * ### Example + * + * ```kotlin + * class Score( + * val student: ObjectId, + * val subject: String, + * val score: Int, + * ) + * + * class Results( + * val passingScores: Int, + * ) + * + * scores.aggregate() + * .match { Score::score gt 99 } + * .countTo(Results::passingScores) + * ``` + */ + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + @KtMongoDsl + fun countTo(field: Field): Pipeline = + withStage(CountStage(field.path, context)).reinterpret() + + /** + * Counts how many elements exist in the pipeline and outputs a single document containing a single [field] containing + * the count. + * + * [field] must be a simple field name. Subfields (accessed through the `/` operator) are forbidden. + * + * ### Example + * + * ```kotlin + * class Score( + * val student: ObjectId, + * val subject: String, + * val score: Int, + * ) + * + * class Results( + * val passingScores: Int, + * ) + * + * scores.aggregate() + * .match { Score::score gt 99 } + * .countTo(Results::passingScores) + * ``` + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun countTo(field: KProperty1): Pipeline = + countTo(with(FieldDslImpl(context)) { field.field }) + +} + +@OptIn(LowLevelApi::class) +private class CountStage( + val path: Path, + context: BsonContext, +) : AbstractBsonNode(context) { + + init { + require(path.parent == null) { "The \$count stage only accepts a simple field name, not nested fields. Found '$path'." } + require(path.segment is PathSegment.Field) { "The \$count stage only accepts fields, not other types of paths. Found $path of type ${path.segment::class}" } + } + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeString("\$count", path.toString()) + } + +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Group.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Group.kt new file mode 100644 index 00000000..2e9d9356 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Group.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Group.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AccumulationOperators +import opensavvy.ktmongo.dsl.aggregation.AccumulationOperatorsImpl +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Pipeline implementing the `$group` stage. + */ +@KtMongoDsl +interface HasGroup : Pipeline { + + /** + * Combines multiple documents into a single document. + * + * The resulting documents contain fields generated by accumulating all input documents. + * To learn more about accumulation operators, see [AccumulationOperators]. + * + * ### Example + * + * If we have users with an account balance, we can find out the total account balance of all users. + * + * ```kotlin + * class User( + * val name: String, + * val balance: Int, + * ) + * + * class Result( + * val totalBalance: Int, + * ) + * + * users.aggregate() + * .group { + * Result::totalBalance sum of(User::balance) + * } + * ``` + * + * To see the list of available accumulation operators, see [AccumulationOperators]. + * + * ### Performance + * + * `$group` is a blocking stage, which causes the pipeline to wait for all input data to be retrieved + * for the blocking stage before processing the data. A blocking stage may reduce performance + * because it reduces parallel processing for a pipeline with multiple stages. + * A blocking stage may also use substantial amounts of memory for large data sets. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/) + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + @KtMongoDsl + fun group( + block: AccumulationOperators.() -> Unit, + ): Pipeline = + withStage(GroupStage(AccumulationOperatorsImpl(context).apply(block), context)) + .reinterpret() + +} + +private class GroupStage( + val operators: AccumulationOperators<*, *>, + context: BsonContext, +) : AbstractBsonNode(context) { + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument("\$group") { + writeNull("_id") + operators.writeTo(this) + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Limit.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Limit.kt new file mode 100644 index 00000000..b7db67ca --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Limit.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Limit.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Pipeline implementing the `$limit` stage. + */ +@KtMongoDsl +interface HasLimit : Pipeline { + + /** + * Limits the number of elements passed to the next stage to [amount]. + * + * ### Using limit with sorted results + * + * Sort results aren't stable with `limit`: if multiple documents are identical, their relative order is undefined + * and may change from one execution to the next. + * + * To avoid surprises, include a unique field in your sort, for example `_id`. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/limit/) + * + * @see HasSkip.skip Skip over an amount of elements. + * @see HasSample.sample Randomly limit the number of elements. + */ + @KtMongoDsl + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + fun limit(amount: Long): Pipeline = + withStage(LimitStage(amount, context)) + + /** + * Limits the number of elements passed to the next stage to [amount]. + * + * ### Using limit with sorted results + * + * Sort results aren't stable with `limit`: if multiple documents are identical, their relative order is undefined + * and may change from one execution to the next. + * + * To avoid surprises, include a unique field in your sort, for example `_id`. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/limit/) + * + * @see HasSkip.skip Skip over an amount of elements. + * @see HasSample.sample Randomly limit the number of elements. + */ + @KtMongoDsl + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + fun limit(amount: Int): Pipeline = + limit(amount.toLong()) +} + +private class LimitStage( + val amount: Long, + context: BsonContext, +) : AbstractBsonNode(context) { + + init { + require(amount >= 0) { "Negative limits are not allowed. Found: $amount" } + } + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeInt64("\$limit", amount) + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Match.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Match.kt new file mode 100644 index 00000000..1bb31a82 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Match.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Match.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Pipeline implementing the `$match` stage. + */ +@KtMongoDsl +interface HasMatch : Pipeline { + + /** + * Filters documents based on a specified [filter]. + * + * Matched documents are passed to the next pipeline stage. + * + * ### Pipeline optimization + * + * Place the `match` call as early in the pipeline as possible. + * Because `match` limits the total number of elements being processed, earlier `match` operations + * minimize the amount of processing down the pipe. + * + * If you place a `match` at the very beginning of a pipeline, the query can take advantage of indexes. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/) + */ + @KtMongoDsl + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + fun match( + filter: FilterQuery.() -> Unit, + ): Pipeline = + withStage(MatchStage(FilterQuery(context).apply(filter), context)) + +} + +private class MatchStage( + val expression: FilterQuery<*>, + context: BsonContext, +) : AbstractBsonNode(context) { + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument("\$match") { + expression.writeTo(this) + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Project.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Project.kt new file mode 100644 index 00000000..19240cc6 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Project.kt @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Project.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.aggregation.Value +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.path.FieldDsl +import opensavvy.ktmongo.dsl.path.Path +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import opensavvy.ktmongo.dsl.tree.AbstractCompoundBsonNode +import opensavvy.ktmongo.dsl.tree.BsonNode +import opensavvy.ktmongo.dsl.tree.CompoundBsonNode +import kotlin.reflect.KProperty1 + +/** + * Pipeline implementing the `$project` stage. + */ +@KtMongoDsl +interface HasProject : Pipeline { + + /** + * Specifies a list of fields which should be kept in the document. + * + * All fields (except `_id`) that are not specified in this stage are unset. + * + * `_id` is kept even if not specified. To exclude it, see [ProjectStageOperators.excludeId]. + * + * ### Difference with MongoDB + * + * In BSON, the `$project` stage can be used either for declaring an allow-list (by including fields, + * and possibly excluding the `_id`) or a block-list (by excluding fields). MongoDB doesn't allow a single stage + * usage to mix both usages. + * + * Because this is confusing, KtMongo splits both of these use-cases into two different methods. + * The former (selecting fields we want to keep) is performed by this method. + * The latter (selecting fields we want to remove) is performed by the stage [`$unset`][HasUnset.unset]. + * + * Note that just like in MongoDB, this stage can use all operators of the [`$set` stage][HasSet.set]. + * + * ### Difference with $set + * + * This stage and the [`$set` stage][HasSet.set] are quite similar. In fact, both stages behave the same for fields + * that are specified. + * + * However, the stages behave differently for fields that are not specified: + * - `$project` removes all fields that are not explicitly specified. + * - `$set` does not impact fields that are not explicitly specified, they are left in the exact same state as previously. + * + * ### Performance + * + * When you use a `$project` stage it should typically be the last stage in your pipeline, + * used to specify which fields to return to the client. + * + * Using a `$project` stage at the beginning or middle of a pipeline to reduce the number of fields + * passed to subsequent pipeline stages is unlikely to improve performance, + * as the database performs this optimization automatically. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val year: Int?, + * ) + * + * users.aggregate() + * .project { + * include(User::name) + * } + * .toList() + * ``` + * + * In this example, the `year` field will not be returned by the aggregation, because it is not mentioned in the `$project` stage. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/) + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + @KtMongoDsl + fun project( + block: ProjectStageOperators.() -> Unit, + ): Pipeline = + withStage(createProjectStage(context, block)) + +} + +internal fun createProjectStage(context: BsonContext, block: ProjectStageOperators.() -> Unit): BsonNode = + ProjectStage(ProjectStageBsonNode(context).apply(block), context) + +private class ProjectStage( + val expression: ProjectStageOperators<*>, + context: BsonContext, +) : AbstractBsonNode(context) { + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument("\$project") { + expression.writeTo(this) + } + } +} + +/** + * The operators allowed in a [`$project` stage][HasProject.project]. + */ +@KtMongoDsl +interface ProjectStageOperators : CompoundBsonNode, AggregationOperators, FieldDsl, SetStageOperators { + + /** + * Excludes the `_id` field. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * users.aggregate() + * .project { + * include(User::age) + * excludeId() + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#_id-field) + */ + @KtMongoDsl + fun excludeId() + + /** + * Explicitly includes [field]. + * + * Note that fields that aren't mentioned in the `$project` stage are deleted (except the `_id` field, which must be [explicitly excluded][excludeId]). + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * users.aggregate() + * .project { + * include(User::age) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#include-fields) + */ + @KtMongoDsl + fun include(field: Field) + + /** + * Explicitly includes [field]. + * + * Note that fields that aren't mentioned in the `$project` stage are deleted (except the `_id` field, which must be [explicitly excluded][excludeId]). + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * users.aggregate() + * .project { + * include(User::age) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#include-fields) + */ + @KtMongoDsl + fun include(field: KProperty1) { + include(field.field) + } + +} + +private class ProjectStageBsonNode( + context: BsonContext, +) : AbstractCompoundBsonNode(context), ProjectStageOperators { + + // region Exclude ID + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun excludeId() { + accept(ProjectExcludeIdBsonNode(context)) + } + + @LowLevelApi + private class ProjectExcludeIdBsonNode( + context: BsonContext, + ) : AbstractBsonNode(context) { + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeBoolean("_id", false) + } + } + + // endregion + // region Include field + + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + override fun include(field: Field) { + accept(ProjectIncludeBsonNode(field.path, context)) + } + + @LowLevelApi + private class ProjectIncludeBsonNode( + val path: Path, + context: BsonContext, + ) : AbstractBsonNode(context) { + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeBoolean(path.toString(), true) + } + } + + // endregion + // region Set field + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun Field.set(value: Value) { + accept(ProjectSetBsonNode(this.path, value, context)) + } + + @LowLevelApi + private class ProjectSetBsonNode( + val path: Path, + val value: Value<*, *>, + context: BsonContext, + ) : AbstractBsonNode(context) { + + override fun write(writer: BsonFieldWriter) = with(writer) { + write(path.toString()) { + value.writeTo(this) + } + } + } + + // endregion + +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Sample.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Sample.kt new file mode 100644 index 00000000..358c0aa4 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Sample.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Sample.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Pipeline implementing the `$sample` stage. + */ +@KtMongoDsl +interface HasSample : Pipeline { + + /** + * Randomly selects [size] documents. + * + * ### Pipeline optimizations + * + * MongoDB is able to perform sampling more efficiently if it is the first stage of the pipeline and [size] is less + * than 5% of the collection size. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/) + * + * @see HasLimit.limit Selects the first elements found. + */ + @KtMongoDsl + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + fun sample(size: Int): Pipeline = + withStage(SampleStage(size, context)) +} + +private class SampleStage( + val size: Int, + context: BsonContext, +) : AbstractBsonNode(context) { + + init { + require(size >= 1) { "The sample size should be at least 1. Found: $size" } + } + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument("\$sample") { + writeInt32("size", size) + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Set.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Set.kt new file mode 100644 index 00000000..04ef8004 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Set.kt @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Set.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.aggregation.Value +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.path.FieldDsl +import opensavvy.ktmongo.dsl.path.Path +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import opensavvy.ktmongo.dsl.tree.AbstractCompoundBsonNode +import opensavvy.ktmongo.dsl.tree.BsonNode +import opensavvy.ktmongo.dsl.tree.CompoundBsonNode +import kotlin.reflect.KProperty1 + +/** + * Pipeline implementing the `$set` stage. + */ +@KtMongoDsl +interface HasSet : Pipeline { + + /** + * Adds new fields to documents, or overwrites existing fields. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/) + */ + @KtMongoDsl + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun set( + block: SetStageOperators.() -> Unit, + ): Pipeline = + withStage(createSetStage(context, block)) + +} + +@OptIn(LowLevelApi::class) +private class SetStage( + val expression: SetStageOperators<*>, + context: BsonContext, +) : AbstractBsonNode(context) { + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument("\$set") { + expression.writeTo(this) + } + } +} + +internal fun createSetStage(context: BsonContext, block: SetStageOperators.() -> Unit): BsonNode = + SetStage(SetStageBsonNode(context).apply(block), context) + +/** + * The operators allowed in a [set] stage. + */ +@KtMongoDsl +interface SetStageOperators : CompoundBsonNode, AggregationOperators, FieldDsl { + + // region $set + + /** + * Replaces the value of a field with the specified [value]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> Field.set(value: Value) + + /** + * Replaces the value of a field with the specified [value]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> KProperty1.set(value: Value) { + this.field.set(value) + } + + /** + * Replaces the value of a field with the specified [value]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> Field.set(value: V) { + this.set(of(value)) + } + + /** + * Replaces the value of a field with the specified [value]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> KProperty1.set(value: V) { + this.field.set(value) + } + + // endregion + // region Conditional $set + // region setIf + + /** + * Replaces the value of a field with the specified [value], if [condition] is `true`. + * + * If [condition] is `false`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field.setIf(condition: Value, value: Value) = + this set cond(condition, value, of(this)) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `true`. + * + * If [condition] is `false`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> KProperty1.setIf(condition: Value, value: Value) = + this.field.setIf(condition, value) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `true`. + * + * If [condition] is `false`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field.setIf(condition: Value, value: V) = + this.setIf(condition, of(value)) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `true`. + * + * If [condition] is `false`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> KProperty1.setIf(condition: Value, value: V) = + this.field.setIf(condition, value) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `true`. + * + * If [condition] is `false`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field.setIf(condition: Boolean, value: Value) { + if (condition) + this.set(value) + } + + /** + * Replaces the value of a field with the specified [value], if [condition] is `true`. + * + * If [condition] is `false`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> KProperty1.setIf(condition: Boolean, value: Value) = + this.field.setIf(condition, value) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `true`. + * + * If [condition] is `false`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field.setIf(condition: Boolean, value: V) = + this.setIf(condition, of(value)) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `true`. + * + * If [condition] is `false`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> KProperty1.setIf(condition: Boolean, value: V) = + this.field.setIf(condition, value) + + // endregion + // region setUnless + + /** + * Replaces the value of a field with the specified [value], if [condition] is `false`. + * + * If [condition] is `true`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field.setUnless(condition: Value, value: Value) = + this set cond(condition, of(this), value) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `false`. + * + * If [condition] is `true`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> KProperty1.setUnless(condition: Value, value: Value) = + this.field.setUnless(condition, value) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `false`. + * + * If [condition] is `true`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field.setUnless(condition: Value, value: V) = + this.setUnless(condition, of(value)) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `false`. + * + * If [condition] is `true`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> KProperty1.setUnless(condition: Value, value: V) = + this.field.setUnless(condition, value) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `false`. + * + * If [condition] is `true`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field.setUnless(condition: Boolean, value: Value) { + if (!condition) + this.set(value) + } + + /** + * Replaces the value of a field with the specified [value], if [condition] is `false`. + * + * If [condition] is `true`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> KProperty1.setUnless(condition: Boolean, value: Value) = + this.field.setUnless(condition, value) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `false`. + * + * If [condition] is `true`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field.setUnless(condition: Boolean, value: V) = + this.setUnless(condition, of(value)) + + /** + * Replaces the value of a field with the specified [value], if [condition] is `false`. + * + * If [condition] is `true`, this operator does nothing. + * + * ### External resources + * + * - [`$set`](https://www.mongodb.com/docs/manual/reference/operator/update/set/) + * - [`$cond`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> KProperty1.setUnless(condition: Boolean, value: V) = + this.field.setUnless(condition, value) + + // endregion + // endregion +} + +private class SetStageBsonNode( + context: BsonContext, +) : AbstractCompoundBsonNode(context), SetStageOperators { + + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + override fun Field.set(value: Value) { + accept(SetBsonNode(this.path, value, context)) + } + + @LowLevelApi + private class SetBsonNode( + val path: Path, + val value: Value<*, *>, + context: BsonContext, + ) : AbstractBsonNode(context) { + + override fun write(writer: BsonFieldWriter) = with(writer) { + write(path.toString()) { + value.writeTo(this) + } + } + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Skip.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Skip.kt new file mode 100644 index 00000000..f0873ad6 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Skip.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Skip.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Pipeline implementing the `$skip` stage. + */ +@KtMongoDsl +interface HasSkip : Pipeline { + + /** + * Skips over the specified [amount] of documents that pass into the stage, + * and passes the remaining documents to the next stage. + * + * ### Using skip with sorted results + * + * Sort results aren't stable with `skip`: if multiple documents are identical, their relative order is undefined + * and may change from one execution to the next. + * + * To avoid surprises, include a unique field in your sort, for example `_id`. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/skip/) + * + * @see HasLimit.limit Limit the number of elements. + */ + @KtMongoDsl + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + fun skip(amount: Long): Pipeline = + withStage(SkipStage(amount, context)) + + /** + * Skips over the specified [amount] of documents that pass into the stage, + * and passes the remaining documents to the next stage. + * + * ### Using skip with sorted results + * + * Sort results aren't stable with `skip`: if multiple documents are identical, their relative order is undefined + * and may change from one execution to the next. + * + * To avoid surprises, include a unique field in your sort, for example `_id`. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/skip/) + * + * @see HasLimit.limit Limit the number of elements. + */ + @KtMongoDsl + fun skip(amount: Int): Pipeline = + skip(amount.toLong()) + +} + +private class SkipStage( + val amount: Long, + context: BsonContext, +) : AbstractBsonNode(context) { + + init { + require(amount >= 0) { "At least 0 elements should be skipped. Found: $amount" } + } + + @LowLevelApi + override fun simplify(): AbstractBsonNode? = + when { + amount == 0L -> null + else -> this + } + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeInt64("\$skip", amount) + } +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Sort.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Sort.kt new file mode 100644 index 00000000..643cb9a1 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Sort.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Sort.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.options.SortOptionDsl +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.path.Path +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import opensavvy.ktmongo.dsl.tree.AbstractCompoundBsonNode +import opensavvy.ktmongo.dsl.tree.BsonNode + +/** + * Pipeline implementing the `$sort` stage. + */ +@KtMongoDsl +interface HasSort : Pipeline { + + /** + * Specifies in which order elements should be sorted. + * + * ### Example + * + * ```kotlin + * collection.aggregate() + * .sort { + * ascending(User::age) + * } + * .limit(15) + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/) + */ + @KtMongoDsl + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + fun sort( + block: SortOptionDsl.() -> Unit, + ): Pipeline = + withStage(SortStage(context).apply(block)) +} + +private class SortStage( + context: BsonContext, +) : AbstractCompoundBsonNode(context), SortOptionDsl { + + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + override fun ascending(field: Field) { + accept(SortBsonNode(field.path, 1, context)) + } + + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + override fun descending(field: Field) { + accept(SortBsonNode(field.path, -1, context)) + } + + @LowLevelApi + override fun simplify(children: List): AbstractBsonNode? = + if (children.isNotEmpty()) this + else null + + @LowLevelApi + override fun write(writer: BsonFieldWriter, children: List) = with(writer) { + writeDocument("\$sort") { + super.write(this, children) + } + } + + @LowLevelApi + private class SortBsonNode( + val path: Path, + val value: Int, + context: BsonContext, + ) : AbstractBsonNode(context) { + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeInt32(path.toString(), value) + } + } + +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/UnionWith.kt b/dsl/src/commonMain/kotlin/aggregation/stages/UnionWith.kt new file mode 100644 index 00000000..7d2a1743 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/UnionWith.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/UnionWith.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Pipeline implementing the `$unionWith` stage. + */ +@KtMongoDsl +interface HasUnionWith : Pipeline { + + /** + * Combines two aggregations into a single result set. + * + * `$unionWith` outputs the combined result set (including duplicates) to the next stage. + * The order in which the combined result set documents are output is unspecified. + * + * ### Namespacing + * + * [other] must be a pipeline from the same namespace. It may be a pipeline from the same collection or another + * collection, as long as they are both part of the same namespace. + * + * ### Example + * + * ```kotlin + * interface Vehicle { + * val brand: String, + * } + * + * class Car( + * override val brand: String, + * val enginePower: Int, + * ) : Vehicle + * + * class Bike( + * override val brand: String, + * val hasBasket: Boolean + * ) + * + * val selectedCars = cars.aggregate() + * .match { Car::enginePower gt 30 } + * .project { include(Car::brand) } + * .reinterpret() + * + * val selectedVehicles = bikes.aggregate() + * .project { include(Bike::brand) } + * .reinterpret() + * .unionWith(selectedCars) + * .limit(5) + * .toList() + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/) + */ + @KtMongoDsl + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + fun unionWith(other: HasUnionWithCompatibility): Pipeline = + withStage(UnionWithStage(other, context)) + +} + +/** + * Pipeline that can be used as the second argument in a `$unionWith` stage. + * + * Instances of this interface should be immutable. + */ +@KtMongoDsl +interface HasUnionWithCompatibility : Pipeline { + + /** + * Writes this pipeline into a `$unionWith` stage. + * + * This method is a low-level API for building custom `$unionWith` stages. Regular users should use [HasUnionWith.unionWith] instead. + * + * ### Implementation contract + * + * When another pipeline wants to embed the current pipeline into itself, it will call this method from within + * the `$unionWith` stage. This method should thus emit the body of the stage: + * + * ```json + * { + * coll: "", + * pipeline: [ ,… ] + * } + * ``` + * + * ### External resources + * + * - [Official documentation of `$unionWith`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/) + * + * @see HasUnionWith.unionWith The `$unionWith` stage. + */ + @LowLevelApi + fun embedInUnionWith(writer: BsonFieldWriter) + +} + +@LowLevelApi +private class UnionWithStage( + val other: HasUnionWithCompatibility<*>, + context: BsonContext, +) : AbstractBsonNode(context) { + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument("\$unionWith") { + other.embedInUnionWith(this) + } + } + +} diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Unset.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Unset.kt new file mode 100644 index 00000000..d13f15fc --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Unset.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/aggregation/stages/Unset.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.aggregation.stages + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AggregationOperators +import opensavvy.ktmongo.dsl.aggregation.Pipeline +import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.path.FieldDsl +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import opensavvy.ktmongo.dsl.tree.BsonNode +import kotlin.reflect.KProperty1 + +/** + * Pipeline implementing the `$unset` stage. + */ +interface HasUnset : Pipeline { + + /** + * Removes fields from documents. + * + * ### Example + * + * We can use this stage to migrate from a schema to a newer schema. + * + * ```kotlin + * class User( + * val name: String, + * val age: Int?, // deprecated + * val birthYear: Int?, // new field + * ) + * + * val currentYear = 2025 + * users.updateManyWithPipeline { + * set { + * User::birthYear set (of(currentYear) - of(age)) + * } + * unset { + * exclude(User::age) + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset) + */ + @KtMongoDsl + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun unset( + block: UnsetStageOperators.() -> Unit, + ): Pipeline = + withStage(createUnsetStage(context, block)) + +} + +internal fun createUnsetStage(context: BsonContext, block: UnsetStageOperators.() -> Unit): BsonNode = + UnsetStage(context).apply { block() } + +/** + * The operators allowed in an [`$unset`][HasUnset.unset] stage. + */ +@KtMongoDsl +interface UnsetStageOperators : BsonNode, AggregationOperators, FieldDsl { + + /** + * Excludes a field from the current document. + * + * ### Example + * + * See [HasUnset.unset]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset) + */ + fun exclude(field: Field) + + /** + * Excludes a field from the current document. + * + * ### Example + * + * See [HasUnset.unset]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset) + */ + fun exclude(field: KProperty1) { + exclude(field.field) + } + +} + +private class UnsetStage( + context: BsonContext, +) : AbstractBsonNode(context), UnsetStageOperators { + + private val fields = HashSet>() + + override fun exclude(field: Field) { + require(!frozen) { "This \$unset stage has already been frozen, it is too late to exclude the field $field" } + fields += field + } + + @LowLevelApi + override fun simplify(): AbstractBsonNode? = + if (fields.isEmpty()) null + else super.simplify() + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("\$unset") { + for (field in fields) { + writeString(field.toString()) + } + } + } +} diff --git a/dsl/src/commonMain/kotlin/command/BulkWrite.kt b/dsl/src/commonMain/kotlin/command/BulkWrite.kt new file mode 100644 index 00000000..f141a793 --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/BulkWrite.kt @@ -0,0 +1,578 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/BulkWrite.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.options.Options +import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import opensavvy.ktmongo.dsl.tree.CompoundNode +import opensavvy.ktmongo.dsl.tree.Node +import opensavvy.ktmongo.dsl.tree.acceptAll + +sealed interface AvailableInBulkWrite : Node, Command + +/** + * Performing multiple write operations in a single request. + * + * ### Example + * + * ```kotlin + * users.bulkWrite { + * updateOne({ User::name eq "foo" }) { + * User::age set 18 + * } + * + * upsertOne({ User::name eq "bob" }) { + * User::age setOnInsert 18 + * User::age inc 1 + * } + * } + * ``` + * + * ### Filtered writes + * + * If we have multiple writes that share a similar filter, we can extract it to be common between them. + * + * ```kotlin + * users.bulkWrite { + * updateOne({ User::name eq "foo" }) { + * User::age set 18 + * } + * + * filtered({ User::isAlive eq true }) { + * updateMany({ User::name eq "bar" }) { + * User::age inc 2 + * } + * + * updateOne({ User::name eq "baz" }) { + * User::age inc 1 + * } + * } + * } + * ``` + * + * To learn more, see [filtered]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/bulkWrite/) + * + * @see BulkWriteOptions Options + */ +@OptIn(LowLevelApi::class) +@KtMongoDsl +class BulkWrite private constructor( + context: BsonContext, + private val globalFilter: FilterQuery.() -> Unit, + val options: BulkWriteOptions, +) : Command, AbstractBsonNode(context), CompoundNode> { + + private val _operations = ArrayList>() + val operations: Sequence> get() = _operations.asSequence() + + constructor(context: BsonContext, globalFilter: FilterQuery.() -> Unit) : this(context, globalFilter, BulkWriteOptions(context)) + + @LowLevelApi + @DangerousMongoApi + override fun accept(node: AvailableInBulkWrite) { + _operations += node + } + + /** + * Declares a [filter] that is shared between all children [operations]. + * + * ### Example + * + * Sometimes, we have multiple operations in a single bulk write that share the same filter. + * This method allows to declare it a single time. + * + * ```kotlin + * users.bulkWrite { + * filtered({ User::isAlive eq true }) { + * updateOne { /* … */ } + * updateOne { /* … */ } + * } + * } + * ``` + */ + fun filtered( + filter: FilterQuery.() -> Unit, + operations: BulkWrite.() -> Unit, + ) { + val parent = this + + val child = BulkWrite( + context = context, + globalFilter = { + parent.globalFilter(this) + filter() + } + ) + + child.operations() + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + acceptAll(child.operations.asIterable()) + } + + /** + * Inserts a [document]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * insertOne(User(name = "Bob", age = 18)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#insertone) + * + * @see insertMany Insert multiple documents. + * @see updateOne Update an existing document. + * @see upsertOne Update or insert a document. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun insertOne( + document: Document, + options: InsertOneOptions.() -> Unit = {}, + ) { + val model = InsertOne(context, document) + + model.options.options() + + accept(model) + } + + /** + * Inserts multiple [documents] in a single operation. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * insertMany(listOf(User(name = "Bob", age = 18), User(name = "Alice", age = 17))) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#insertone) + * + * @see insertOne Insert a single document. + * @see updateMany Update multiple documents. + */ + fun insertMany( + documents: Iterable, + options: InsertOneOptions.() -> Unit = {}, + ) { + for (document in documents) { + insertOne(document, options) + } + } + + /** + * Inserts multiple [documents] in a single operation. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * insertMany(User(name = "Bob", age = 18), User(name = "Alice", age = 17)) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#insertone) + * + * @see insertOne Insert a single document. + * @see updateMany Update multiple documents. + */ + fun insertMany( + vararg documents: Document, + options: InsertOneOptions.() -> Unit = {}, + ) { + insertMany(documents.asList(), options) + } + + /** + * Updates all documents that match [filter] according to [update]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * updateMany( + * filter = { User::name eq "Patrick" }, + * update = { + * User::age set 15 + * } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#updateone-and-updatemany) + * + * @see updateOne Update a single document. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun updateMany( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpdateQuery.() -> Unit, + ) { + val model = UpdateMany(context) + + model.options.options() + model.filter.globalFilter() + model.filter.filter() + model.update.update() + + accept(model) + } + + /** + * Updates a single document that matches [filter] according to [update]. + * + * If multiple documents match [filter], only the first one found is updated. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * updateOne( + * filter = { User::name eq "Patrick" }, + * update = { + * User::age set 15 + * } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#updateone-and-updatemany) + * + * @see updateMany Update multiple documents. + * @see insertOne Create a new document. + * @see upsertOne Create a document if none are found. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun updateOne( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpdateQuery.() -> Unit, + ) { + val model = UpdateOne(context) + + model.options.options() + model.filter.globalFilter() + model.filter.filter() + model.update.update() + + accept(model) + } + + /** + * Updates a single document that matches [filter] according to [update]. + * + * If multiple documents match [filter], only the first one found is updated. + * + * If no documents match [filter], a new one is created. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * upsertOne( + * filter = { User::name eq "Patrick" }, + * update = { + * User::age set 15 + * } + * ) + * } + * ``` + * + * If a document exists that has the `name` of "Patrick", its age is set to 15. + * If none exist, a document with `name` "Patrick" and `age` 15 is created. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#updateone-and-updatemany) + * - [The behavior of upsert functions](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/#insert-a-new-document-if-no-match-exists--upsert-) + * + * @see insertOne Always create a new document. + * @see updateOne Do nothing if no matching documents are found. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun upsertOne( + options: UpdateOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + update: UpsertQuery.() -> Unit, + ) { + val model = UpsertOne(context) + + model.options.options() + model.filter.globalFilter() + model.filter.filter() + model.update.update() + + accept(model) + } + + /** + * Replaces a document that matches [filter] by [document]. + * + * If multiple documents match [filter], only the first one found is updated. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * replaceOne( + * filter = { User::name eq "Patrick" }, + * document = User("Bob", 15) + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#replaceone) + * + * @see updateOne Update an existing document. + * @see repsertOne Replace a document, or insert it if it doesn't exist. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun replaceOne( + options: ReplaceOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + document: Document, + ) { + val model = ReplaceOne(context, document) + + model.options.options() + model.filter.globalFilter() + model.filter.filter() + + accept(model) + } + + /** + * Replaces a document that matches [filter] by [document]. + * + * If multiple documents match [filter], only the first one found is updated. + * + * If no documents match [filter], [document] is inserted. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * repsertOne( + * filter = { User::name eq "Patrick" }, + * document = User("Bob", 15) + * ) + * } + * ``` + * + * If a document exists that has the `name` of "Patrick", it is replaced by the new document. + * If none exist, the document is inserted. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#replaceone) + * - [The behavior of upsert functions](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/#insert-a-new-document-if-no-match-exists--upsert-) + * + * @see replaceOne Replace an existing document. + * @see insertOne Always create a new document. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun repsertOne( + options: ReplaceOptions.() -> Unit = {}, + filter: FilterQuery.() -> Unit = {}, + document: Document, + ) { + val model = RepsertOne(context, document) + + model.options.options() + model.filter.globalFilter() + model.filter.filter() + + accept(model) + } + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeInt32("bulkWrite", 1) + + writeArray("ops") { + for (operation in _operations) { + writeDocument { + when (operation) { + is InsertOne<*> -> { + writeInt32("insert", 0) + writeObjectSafe("document", operation.document) + operation.options.writeTo(this) + } + + is UpdateOne<*> -> { + writeInt32("update", 0) + writeDocument("filter") { + operation.filter.writeTo(this) + } + writeDocument("updateMods") { + operation.update.writeTo(this) + } + writeBoolean("multi", false) + operation.options.writeTo(this) + } + + is ReplaceOne<*> -> { + writeInt32("update", 0) + writeDocument("filter") { + operation.filter.writeTo(this) + } + writeObjectSafe("updateMods", operation.document) + writeBoolean("multi", false) + operation.options.writeTo(this) + } + + is RepsertOne<*> -> { + writeInt32("update", 0) + writeDocument("filter") { + operation.filter.writeTo(this) + } + writeObjectSafe("updateMods", operation.document) + writeBoolean("multi", false) + writeBoolean("upsert", true) + operation.options.writeTo(this) + } + + is UpsertOne<*> -> { + writeInt32("update", 0) + writeDocument("filter") { + operation.filter.writeTo(this) + } + writeDocument("updateMods") { + operation.update.writeTo(this) + } + writeBoolean("upsert", true) + writeBoolean("multi", false) + operation.options.writeTo(this) + } + + is UpdateMany<*> -> { + writeInt32("update", 0) + writeDocument("filter") { + operation.filter.writeTo(this) + } + writeDocument("updateMods") { + operation.update.writeTo(this) + } + writeBoolean("multi", true) + operation.options.writeTo(this) + } + + is DeleteOne<*> -> { + writeInt32("delete", 0) + writeDocument("filter") { + operation.filter.writeTo(this) + } + writeBoolean("multi", false) + operation.options.writeTo(this) + } + + is DeleteMany<*> -> { + writeInt32("delete", 0) + writeDocument("filter") { + operation.filter.writeTo(this) + } + writeBoolean("multi", true) + operation.options.writeTo(this) + } + } + } + } + } + + options.writeTo(this) + } +} + +/** + * The options for a [BulkWrite] command. + */ +class BulkWriteOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/Command.kt b/dsl/src/commonMain/kotlin/command/Command.kt new file mode 100644 index 00000000..b9b9afcc --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/Command.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/Command.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.dsl.tree.BsonNode + +/** + * A command that can be sent to a MongoDB server. + */ +interface Command : BsonNode diff --git a/dsl/src/commonMain/kotlin/command/Count.kt b/dsl/src/commonMain/kotlin/command/Count.kt new file mode 100644 index 00000000..5b938a9a --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/Count.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/Count.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.options.* +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Counting a number of documents in a collection. + * + * ### Example + * + * ```kotlin + * users.count({ limit(99) }) { + * User::age lt 18 + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/count/) + * + * @see FilterQuery Filter operators + * @see CountOptions Options + */ +@KtMongoDsl +class Count private constructor( + context: BsonContext, + val options: CountOptions, + val filter: FilterQuery, +) : Command, AbstractBsonNode(context) { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, CountOptions(context), FilterQuery(context)) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument("query") { + filter.writeTo(this) + } + + options.writeTo(this) + } +} + +/** + * The options for a [Count] command. + */ +@OptIn(LowLevelApi::class) +class CountOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithLimit, + WithSkip, + WithMaxTime diff --git a/dsl/src/commonMain/kotlin/command/Delete.kt b/dsl/src/commonMain/kotlin/command/Delete.kt new file mode 100644 index 00000000..6c5dc351 --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/Delete.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/Delete.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.options.Options +import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Deleting a single document from a collection. + * + * ### Example + * + * ```kotlin + * users.deleteOne { + * User::name eq "Patrick" + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/delete/) + */ +@KtMongoDsl +class DeleteOne private constructor( + context: BsonContext, + val options: DeleteOneOptions, + val filter: FilterQuery, +) : AbstractBsonNode(context), Command, AvailableInBulkWrite { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, DeleteOneOptions(context), FilterQuery(context)) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("deletes") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + writeInt32("limit", 1) + } + } + + options.writeTo(this) + } +} + +/** + * Deleting multiple documents from a collection. + * + * ### Example + * + * ```kotlin + * users.deleteMany { + * User::age gte 200 + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/delete/) + */ +@KtMongoDsl +class DeleteMany private constructor( + context: BsonContext, + val options: DeleteManyOptions, + val filter: FilterQuery, +) : AbstractBsonNode(context), Command, AvailableInBulkWrite { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, DeleteManyOptions(context), FilterQuery(context)) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("deletes") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + } + } + + options.writeTo(this) + } +} + +/** + * The options for a [DeleteOne] command. + */ +class DeleteOneOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithWriteConcern + +/** + * The options for a [DeleteMany] command. + */ +class DeleteManyOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/Drop.kt b/dsl/src/commonMain/kotlin/command/Drop.kt new file mode 100644 index 00000000..643d87fd --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/Drop.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/Drop.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.options.Options +import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Deleting an entire collection at once. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/drop/) + */ +@KtMongoDsl +class Drop private constructor( + context: BsonContext, + val options: DropOptions, +) : Command, AbstractBsonNode(context) { + + constructor(context: BsonContext) : this(context, DropOptions(context)) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + options.writeTo(this) + } +} + +/** + * The options for a [Drop] command. + */ +@OptIn(LowLevelApi::class) +class DropOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/Find.kt b/dsl/src/commonMain/kotlin/command/Find.kt new file mode 100644 index 00000000..8e47c244 --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/Find.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/Find.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.options.* +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Searching for documents in a collection. + * + * ### Example + * + * ```kotlin + * users.find(options = { limit(12) }) { + * User::age lt 18 + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/find/) + * + * @see FilterQuery Filter operators + * @see FindOptions Options + */ +@KtMongoDsl +class Find private constructor( + context: BsonContext, + val options: FindOptions, + val filter: FilterQuery, +) : Command, AbstractBsonNode(context) { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, FindOptions(context), FilterQuery(context)) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument("filter") { + filter.writeTo(this) + } + + options.writeTo(this) + } +} + +/** + * The options for a [Find] command. + */ +@OptIn(LowLevelApi::class) +class FindOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithLimit, + WithSkip, + WithMaxTime, + WithSort, + WithReadConcern, + WithReadPreference diff --git a/dsl/src/commonMain/kotlin/command/Insert.kt b/dsl/src/commonMain/kotlin/command/Insert.kt new file mode 100644 index 00000000..746b8920 --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/Insert.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/Insert.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.options.Options +import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Inserting a single element in a collection. + * + * ### Example + * + * ```kotlin + * users.insertOne(User(name = "Bob", age = 18)) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/insert/) + * + * @see InsertMany + * @see InsertOneOptions Options + */ +@KtMongoDsl +class InsertOne private constructor( + context: BsonContext, + val options: InsertOneOptions, + val document: Document, +) : Command, AbstractBsonNode(context), AvailableInBulkWrite { + + constructor(context: BsonContext, document: Document) : this(context, InsertOneOptions(context), document) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("documents") { + writeObjectSafe(document) + } + + options.writeTo(this) + } +} + +/** + * Inserting multiple elements in a collection in a single operation. + * + * ### Example + * + * ```kotlin + * users.insertMany(User(name = "Bob", age = 18), User(name = "Fred", age = 19), User(name = "Arthur", age = 22)) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/insert/) + * + * @see InsertOne + * @see InsertManyOptions Options + */ +@KtMongoDsl +class InsertMany private constructor( + context: BsonContext, + val options: InsertManyOptions, + val documents: List, +) : Command, AbstractBsonNode(context) { + + constructor(context: BsonContext, documents: List) : this(context, InsertManyOptions(context), documents) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("documents") { + for (document in documents) { + writeObjectSafe(document) + } + } + + options.writeTo(this) + } +} + +/** + * The options for a `collection.insertOne` operation. + */ +class InsertOneOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithWriteConcern + +/** + * The options for a `collection.insertMany` operation. + */ +class InsertManyOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/Replace.kt b/dsl/src/commonMain/kotlin/command/Replace.kt new file mode 100644 index 00000000..6d57ba13 --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/Replace.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/Replace.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.options.Options +import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Replaces a single element in a collection. + * + * ### Example + * + * ```kotlin + * users.replaceOne({ User::name eq "foo" }, User("Bob", 15)) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * + * @see FilterQuery Filter operators + */ +@KtMongoDsl +class ReplaceOne private constructor( + context: BsonContext, + val options: ReplaceOptions, + val filter: FilterQuery, + val document: Document, +) : AbstractBsonNode(context), Command, AvailableInBulkWrite { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext, document: Document) : this(context, ReplaceOptions(context), FilterQuery(context), document) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("updates") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + writeObjectSafe("u", document) + writeBoolean("upsert", false) + writeBoolean("multi", false) + } + } + + options.writeTo(this) + } +} + +/** + * Replaces a single element in a collection, or inserts it if it doesn't exist. + * + * ### Example + * + * ```kotlin + * users.replaceOrInsertOne({ User::name eq "foo" }, User("Bob", 15)) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * + * @see FilterQuery Filter operators + */ +@KtMongoDsl +class RepsertOne private constructor( + context: BsonContext, + val options: ReplaceOptions, + val filter: FilterQuery, + val document: Document, +) : AbstractBsonNode(context), Command, AvailableInBulkWrite { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext, document: Document) : this(context, ReplaceOptions(context), FilterQuery(context), document) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("updates") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + writeObjectSafe("u", document) + writeBoolean("upsert", true) + writeBoolean("multi", false) + } + } + + options.writeTo(this) + } +} + +/** + * The options for a [ReplaceOne] operation. + */ +class ReplaceOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/Update.kt b/dsl/src/commonMain/kotlin/command/Update.kt new file mode 100644 index 00000000..d8a91a10 --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/Update.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/Update.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.options.Options +import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateQuery +import opensavvy.ktmongo.dsl.query.UpsertQuery +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Updating a single element in a collection. + * + * ### Example + * + * ```kotlin + * users.updateOne({ User::name eq "foo" }) { + * User::age set 18 + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * + * @see FilterQuery Filter operators + * @see UpdateQuery Update operators + */ +@KtMongoDsl +class UpdateOne private constructor( + context: BsonContext, + val options: UpdateOptions, + val filter: FilterQuery, + val update: UpdateQuery, +) : AbstractBsonNode(context), Command, AvailableInBulkWrite { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, UpdateOptions(context), FilterQuery(context), UpdateQuery(context)) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("updates") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + writeDocument("u") { + update.writeTo(this) + } + writeBoolean("upsert", false) + writeBoolean("multi", false) + } + } + + options.writeTo(this) + } +} + +/** + * Updating a single element in a collection, creating it if it doesn't exist. + * + * ### Example + * + * ```kotlin + * users.upsertOne({ User::name eq "foo" }) { + * User::age set 18 + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * + * @see FilterQuery Filter operators + * @see UpdateQuery Update operators + */ +@KtMongoDsl +class UpsertOne private constructor( + context: BsonContext, + val options: UpdateOptions, + val filter: FilterQuery, + val update: UpsertQuery, +) : Command, AbstractBsonNode(context), AvailableInBulkWrite { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, UpdateOptions(context), FilterQuery(context), UpsertQuery(context)) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("updates") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + writeDocument("u") { + update.writeTo(this) + } + writeBoolean("upsert", true) + writeBoolean("multi", false) + } + } + + options.writeTo(this) + } +} + +/** + * Updating multiple elements in a collection. + * + * ### Example + * + * ```kotlin + * users.updateMany({ User::name eq "foo" }) { + * User::age set 18 + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * + * @see FilterQuery Filter operators + * @see UpdateQuery Update operators + */ +@KtMongoDsl +class UpdateMany private constructor( + context: BsonContext, + val options: UpdateOptions, + val filter: FilterQuery, + val update: UpdateQuery, +) : Command, AbstractBsonNode(context), AvailableInBulkWrite { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, UpdateOptions(context), FilterQuery(context), UpdateQuery(context)) + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("updates") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + writeDocument("u") { + update.writeTo(this) + } + writeBoolean("upsert", false) + writeBoolean("multi", true) + } + } + + options.writeTo(this) + } +} + +/** + * The options for a [UpdateOne], [UpsertOne], [UpdateMany] operation. + */ +class UpdateOptions(context: BsonContext) : + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/UpdateWithPipeline.kt b/dsl/src/commonMain/kotlin/command/UpdateWithPipeline.kt new file mode 100644 index 00000000..f79418d7 --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/UpdateWithPipeline.kt @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/UpdateWithPipeline.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.query.FilterQuery +import opensavvy.ktmongo.dsl.query.UpdateQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery +import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQueryImpl +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode + +/** + * Updating a single element in a collection, using a pipeline. + * + * ### Example + * + * ```kotlin + * users.updateOneWithPipeline({ User::name eq "foo" }) { + * set { + * User::age set 18 + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * + * @see FilterQuery Filter operators + * @see UpdateQuery Update operators + */ +@KtMongoDsl +class UpdateOneWithPipeline private constructor( + context: BsonContext, + val options: UpdateOptions, + val filter: FilterQuery, + val update: UpdateWithPipelineQuery, +) : Command, AbstractBsonNode(context) { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, UpdateOptions(context), FilterQuery(context), UpdateWithPipelineQuery(context)) + + @LowLevelApi + val updates + get() = (update as UpdateWithPipelineQueryImpl<*>).stages + .map { context.buildDocument { it.writeTo(this) } } + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("updates") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + writeArray("u") { + for (update in (update as UpdateWithPipelineQueryImpl<*>).stages) { + writeDocument { + update.writeTo(this) + } + } + } + writeBoolean("upsert", false) + writeBoolean("multi", false) + } + } + + options.writeTo(this) + } +} + +/** + * Updating a single element in a collection, creating it if it doesn't exist, using a pipeline. + * + * ### Example + * + * ```kotlin + * users.upsertOneWithPipeline({ User::name eq "foo" }) { + * set { + * User::age set 18 + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * + * @see FilterQuery Filter operators + * @see UpdateQuery Update operators + */ +@KtMongoDsl +class UpsertOneWithPipeline private constructor( + context: BsonContext, + val options: UpdateOptions, + val filter: FilterQuery, + val update: UpdateWithPipelineQuery, +) : Command, AbstractBsonNode(context) { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, UpdateOptions(context), FilterQuery(context), UpdateWithPipelineQuery(context)) + + @LowLevelApi + val updates + get() = (update as UpdateWithPipelineQueryImpl<*>).stages + .map { context.buildDocument { it.writeTo(this) } } + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("updates") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + writeArray("u") { + for (update in (update as UpdateWithPipelineQueryImpl<*>).stages) { + writeDocument { + update.writeTo(this) + } + } + } + writeBoolean("upsert", true) + writeBoolean("multi", false) + } + } + + options.writeTo(this) + } +} + +/** + * Updating multiple elements in a collection, using a pipeline. + * + * ### Example + * + * ```kotlin + * users.updateManyWithPipeline({ User::name eq "foo" }) { + * set { + * User::age set 18 + * } + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/update/) + * + * @see FilterQuery Filter operators + * @see UpdateQuery Update operators + */ +@KtMongoDsl +class UpdateManyWithPipeline private constructor( + context: BsonContext, + val options: UpdateOptions, + val filter: FilterQuery, + val update: UpdateWithPipelineQuery, +) : Command, AbstractBsonNode(context) { + + @OptIn(LowLevelApi::class) + constructor(context: BsonContext) : this(context, UpdateOptions(context), FilterQuery(context), UpdateWithPipelineQuery(context)) + + @LowLevelApi + val updates + get() = (update as UpdateWithPipelineQueryImpl<*>).stages + .map { context.buildDocument { it.writeTo(this) } } + + @LowLevelApi + override fun write(writer: BsonFieldWriter) = with(writer) { + writeArray("updates") { + writeDocument { + writeDocument("q") { + filter.writeTo(this) + } + writeArray("u") { + for (update in (update as UpdateWithPipelineQueryImpl<*>).stages) { + writeDocument { + update.writeTo(this) + } + } + } + writeBoolean("upsert", false) + writeBoolean("multi", true) + } + } + + options.writeTo(this) + } +} diff --git a/dsl/src/commonMain/kotlin/options/LimitOption.kt b/dsl/src/commonMain/kotlin/options/LimitOption.kt new file mode 100644 index 00000000..56fa38d7 --- /dev/null +++ b/dsl/src/commonMain/kotlin/options/LimitOption.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/options/LimitOption.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.options + +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.LowLevelApi + +/** + * Maximum number of elements analyzed by this operation. + * + * For more information, see [WithLimit]. + */ +class LimitOption( + val limit: Long, + context: BsonContext, +) : AbstractOption("limit", context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeInt64(limit) + } +} + +/** + * Limits the number of elements returned by a query. + * + * See [limit]. + */ +interface WithLimit : Options { + + /** + * The maximum number of matching documents to return. + * + * ```kotlin + * collections.count { + * options { + * limit(99) + * } + * } + * ``` + */ + fun limit(limit: Int) { + limit(limit.toLong()) + } + + /** + * The maximum number of matching documents to return. + * + * ```kotlin + * collections.count { + * options { + * limit(99L) + * } + * } + * ``` + * + * Note that not all drivers support specifying a limit larger than an `Int`. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun limit(limit: Long) { + accept(LimitOption(limit, context)) + } + +} diff --git a/dsl/src/commonMain/kotlin/options/MaxTime.kt b/dsl/src/commonMain/kotlin/options/MaxTime.kt new file mode 100644 index 00000000..3819ae5b --- /dev/null +++ b/dsl/src/commonMain/kotlin/options/MaxTime.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/options/MaxTime.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.options + +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.time.Duration + +/** + * Maximum [timeout] spent processing the request. + * + * For more information, see [WithMaxTime]. + */ +class MaxTimeOption( + val timeout: Duration, + context: BsonContext, +) : AbstractOption("maxTimeMS", context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeInt64(timeout.inWholeMilliseconds) + } +} + +/** + * Maximum duration spent processing the request. + * + * See [maxTime]. + */ +interface WithMaxTime : Options { + + /** + * Specifies a maximum amount of time for processing the request. + * + * ```kotlin + * collections.count { + * options { + * maxTime(10.seconds) + * } + * } + * ``` + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun maxTime(timeout: Duration) { + accept(MaxTimeOption(timeout, context)) + } + +} diff --git a/dsl/src/commonMain/kotlin/options/Options.kt b/dsl/src/commonMain/kotlin/options/Options.kt new file mode 100644 index 00000000..a503da6a --- /dev/null +++ b/dsl/src/commonMain/kotlin/options/Options.kt @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2024-2026, OpenSavvy and contributors. + * + * 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/options/Options.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.options + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.bson.BsonValue +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.BsonContext +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.command.Count +import opensavvy.ktmongo.dsl.command.CountOptions +import opensavvy.ktmongo.dsl.tree.AbstractBsonNode +import opensavvy.ktmongo.dsl.tree.AbstractCompoundBsonNode +import opensavvy.ktmongo.dsl.tree.BsonNode +import opensavvy.ktmongo.dsl.tree.CompoundBsonNode + +/** + * Additional parameters that are passed to MongoDB operations. + * + * Options are usually configured with the `options = {}` optional parameter in a command. + * For example, if we want to know how many notifications a user has, but can only display "99" because of UI size + * constraints, we can use the following command: + * ```kotlin + * notifications.count( + * options = { + * limit(99) + * } + * ) { + * Notification::ownedBy eq currentUser + * } + * ``` + * + * If the same option is specified multiple times, only the very last one applies: + * ```kotlin + * notifications.count { + * options { + * limit(99) + * limit(10) + * } + * } + * ``` + * will only count at most 10 elements. + * + * ### Accessing the current value of an option + * + * See [Options.allOptions] and [option]. + * + * ### Implementing this interface + * + * Implementations of this interface must be careful to respect the contract of [BsonNode], in particular about + * the [toString] representation. + * + * Option implementations must be immutable. If the user wants to change an option, they can specify it a second time + * (which will override the previous one). + */ +interface Option : BsonNode { + + /** + * The name of this option, as it appears in the BSON representation. + * + * Options always have the form: + * ```json + * find( + * { + * "limit": 10, + * "sort": { } + * }, + * { } + * ) + * ``` + * + * In this example, the [LimitOption] has the name `"limit"` and the [SortOption] has the name `"sort"`. + * + * ### Implementation notes + * + * This value should be immutable. + */ + val name: String + + /** + * Reads the value of this option. + * + * ### Performance + * + * Note that this method requires to write this option into a temporary BSON value. + */ + @OptIn(LowLevelApi::class) + fun read(): BsonValue + +} + +/** + * Helper to implement [Option]. + */ +abstract class AbstractOption( + override val name: String, + context: BsonContext, +) : AbstractBsonNode(context), Option { + + init { + @OptIn(LowLevelApi::class) + freeze() + } + + @LowLevelApi + protected abstract fun write(writer: BsonValueWriter) + + @LowLevelApi + final override fun write(writer: BsonFieldWriter) = with(writer) { + write(name) { + write(this) + } + } + + @LowLevelApi + final override fun read(): BsonValue = + this.toBson()[name]!! // safe because we always write with that same name +} + +/** + * Utility to easily implement options that contain a document as [content]. + */ +abstract class AbstractCompoundOption( + name: String, + content: BsonNode, + context: BsonContext, +) : AbstractOption(name, context) { + + @OptIn(LowLevelApi::class) + private val content = content.simplify() + ?.apply { freeze() } + + @LowLevelApi + final override fun simplify(): AbstractBsonNode? = + this.takeUnless { content == null } + + @LowLevelApi + final override fun write(writer: BsonValueWriter) = with(writer) { + if (content != null) { + writeDocument { + content.writeTo(this) + } + } + } +} + +/** + * Parent interface for all option containers. + * + * Option containers are types that declare a set of options. They are usually tied to a specific MongoDB [command]. + * + * For example, for options related to the [Count] command, see [CountOptions]. + */ +@KtMongoDsl +interface Options : CompoundBsonNode { + + /** + * The full list of options set on this container. + * + * Specific options are usually searched using the [option] extension. + */ + @LowLevelApi + val allOptions: List