From 59740316bba9adf79a19bb013757d1b76a54a082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 1 Mar 2025 15:46:08 +0100 Subject: [PATCH 1/7] feat(dsl): Add the $filter aggregation operator --- .../commonMain/kotlin/aggregation/ValueDsl.kt | 4 + .../operators/ArrayValueOperators.kt | 250 ++++++++++++++++++ .../aggregation/AggregationTestUtils.kt | 1 + .../operators/ArrayValueOperatorsTest.kt | 128 +++++++++ 4 files changed, 383 insertions(+) create mode 100644 dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt create mode 100644 dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt diff --git a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt index eb304177..4d1dd501 100644 --- a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt +++ b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt @@ -94,6 +94,9 @@ import opensavvy.ktmongo.dsl.path.Field * - [`$ceil`][ArithmeticValueOperators.ceil] * - [`$concat`][ArithmeticValueOperators.concat] * + * Array operators: + * - [`$filter`][ArrayValueOperators.filter] + * * Trigonometric operators and angle management: * - [`$acos`][TrigonometryValueOperators.acos] * - [`$acosh`][TrigonometryValueOperators.acosh] @@ -114,6 +117,7 @@ import opensavvy.ktmongo.dsl.path.Field */ @KtMongoDsl interface ValueDsl : ValueOperators, + ArrayValueOperators, ComparisonValueOperators, ConditionalValueOperators, ArithmeticValueOperators, 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..79fde0a4 --- /dev/null +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2025, 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. + */ + +package opensavvy.ktmongo.dsl.aggregation.operators + +import opensavvy.ktmongo.bson.BsonContext +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.aggregation.AbstractValue +import opensavvy.ktmongo.dsl.aggregation.Value +import opensavvy.ktmongo.dsl.aggregation.ValueDsl +import opensavvy.ktmongo.dsl.path.Field +import kotlin.reflect.KProperty1 + +/** + * Operators to manipulate arrays. + * + * To learn more about aggregation operators, see [opensavvy.ktmongo.dsl.aggregation.ValueDsl]. + */ +interface ArrayValueOperators : ValueOperators { + + // 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: ValueDsl.(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: ValueDsl.(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: ValueDsl.(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: ValueDsl.(Value) -> Value, + ): Value> = + of(this).filter(limit, variableName, predicate) + + @LowLevelApi + private class PredicateEvaluator(override val context: BsonContext) : ValueDsl + + @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 + +} diff --git a/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt index 54807acd..d3188505 100644 --- a/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt +++ b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt @@ -35,6 +35,7 @@ val concat = "\$concat" val sort = "\$sort" val unset = "\$unset" val project = "\$project" +val filter = "\$filter" @OptIn(LowLevelApi::class) class TestPipeline( diff --git a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt new file mode 100644 index 00000000..db7f035e --- /dev/null +++ b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025, 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. + */ + +package opensavvy.ktmongo.dsl.aggregation.operators.aggregation.operators + +import opensavvy.ktmongo.dsl.aggregation.* +import opensavvy.ktmongo.dsl.expr.filter.gt +import opensavvy.prepared.runner.kotest.PreparedSpec + +class ArrayValueOperatorsTest : PreparedSpec({ + + class Target( + val numbers: List, + val results: List + ) + + val numbers = "\$numbers" + val arrayThis = "$\$this" + + suite(filter) { + test("Usage with a list of integers") { + TestPipeline() + .set { + Target::results set Target::numbers + .filter { + it gt of(3) + } + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$filter": { + "input": "$numbers", + "as": "this", + "cond": { + "$gt": [ + "$arrayThis", + {"$literal": 3} + ] + } + } + } + } + } + ] + """.trimIndent()) + } + + test("Usage with a limit") { + TestPipeline() + .set { + Target::results set Target::numbers + .filter(limit = of(4)) { + it gt of(3) + } + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$filter": { + "input": "$numbers", + "as": "this", + "cond": { + "$gt": [ + "$arrayThis", + {"$literal": 3} + ] + }, + "limit": {"$literal": 4} + } + } + } + } + ] + """.trimIndent()) + } + + test("Usage with another variable name") { + val foo = "$\$foo" + + TestPipeline() + .set { + Target::results set Target::numbers + .filter(variableName = "foo") { + it gt of(3) + } + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$filter": { + "input": "$numbers", + "as": "foo", + "cond": { + "$gt": [ + "$foo", + {"$literal": 3} + ] + } + } + } + } + } + ] + """.trimIndent()) + } + } + +}) -- 2.51.2 From 721b1b07e718499080c9f7871d4a5dc3f9d4a214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 1 Mar 2025 16:06:13 +0100 Subject: [PATCH 2/7] feat(dsl): Add the $firstN aggregation operator --- .../commonMain/kotlin/aggregation/ValueDsl.kt | 1 + .../operators/ArrayValueOperators.kt | 146 ++++++++++++++++++ .../aggregation/AggregationTestUtils.kt | 1 + .../operators/ArrayValueOperatorsTest.kt | 24 +++ 4 files changed, 172 insertions(+) diff --git a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt index 4d1dd501..cbe2a287 100644 --- a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt +++ b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt @@ -96,6 +96,7 @@ import opensavvy.ktmongo.dsl.path.Field * * Array operators: * - [`$filter`][ArrayValueOperators.filter] + * - [`$firstN`][ArrayValueOperators.take] * * Trigonometric operators and angle management: * - [`$acos`][TrigonometryValueOperators.acos] diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt index 79fde0a4..6b0c5e55 100644 --- a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt @@ -246,5 +246,151 @@ interface ArrayValueOperators : ValueOperators { } // 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 } diff --git a/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt index d3188505..0e8a562f 100644 --- a/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt +++ b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt @@ -36,6 +36,7 @@ val sort = "\$sort" val unset = "\$unset" val project = "\$project" val filter = "\$filter" +val firstN = "\$firstN" @OptIn(LowLevelApi::class) class TestPipeline( diff --git a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt index db7f035e..64ccd6a3 100644 --- a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt +++ b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt @@ -125,4 +125,28 @@ class ArrayValueOperatorsTest : PreparedSpec({ } } + suite(firstN) { + test("Usage with a list of integers") { + TestPipeline() + .set { + Target::results set Target::numbers + .take(of(5)) + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$firstN": { + "input": "$numbers", + "n": {"$literal": 5} + } + } + } + } + ] + """.trimIndent()) + } + } + }) -- 2.51.2 From a57eccf58d6238103f6ab39ddffb9ac5992392d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 1 Mar 2025 16:17:20 +0100 Subject: [PATCH 3/7] feat(dsl): Add the $lastN aggregation operator --- .../commonMain/kotlin/aggregation/ValueDsl.kt | 1 + .../operators/ArrayValueOperators.kt | 149 ++++++++++++++++++ .../aggregation/AggregationTestUtils.kt | 1 + .../operators/ArrayValueOperatorsTest.kt | 24 +++ 4 files changed, 175 insertions(+) diff --git a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt index cbe2a287..b24ed266 100644 --- a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt +++ b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt @@ -97,6 +97,7 @@ import opensavvy.ktmongo.dsl.path.Field * Array operators: * - [`$filter`][ArrayValueOperators.filter] * - [`$firstN`][ArrayValueOperators.take] + * - [`$lastN`][ArrayValueOperators.takeLast] * * Trigonometric operators and angle management: * - [`$acos`][TrigonometryValueOperators.acos] diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt index 6b0c5e55..14628b3e 100644 --- a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt @@ -392,5 +392,154 @@ interface ArrayValueOperators : ValueOperators { } // 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 } diff --git a/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt index 0e8a562f..8d5e225a 100644 --- a/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt +++ b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt @@ -37,6 +37,7 @@ val unset = "\$unset" val project = "\$project" val filter = "\$filter" val firstN = "\$firstN" +val lastN = "\$lastN" @OptIn(LowLevelApi::class) class TestPipeline( diff --git a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt index 64ccd6a3..c5dffe8c 100644 --- a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt +++ b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt @@ -149,4 +149,28 @@ class ArrayValueOperatorsTest : PreparedSpec({ } } + suite(lastN) { + test("Usage with a list of integers") { + TestPipeline() + .set { + Target::results set Target::numbers + .takeLast(of(5)) + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$lastN": { + "input": "$numbers", + "n": {"$literal": 5} + } + } + } + } + ] + """.trimIndent()) + } + } + }) -- 2.51.2 From f736c402d92fddb4195e553648c38f58999dd6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 1 Mar 2025 16:58:43 +0100 Subject: [PATCH 4/7] feat(dsl): Add the $sortArray aggregation operator (sort by fields) --- .../commonMain/kotlin/aggregation/ValueDsl.kt | 1 + .../operators/ArrayValueOperators.kt | 223 ++++++++++++++++++ .../aggregation/AggregationTestUtils.kt | 1 + .../operators/ArrayValueOperatorsTest.kt | 37 +++ 4 files changed, 262 insertions(+) diff --git a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt index b24ed266..81bcd013 100644 --- a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt +++ b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt @@ -98,6 +98,7 @@ import opensavvy.ktmongo.dsl.path.Field * - [`$filter`][ArrayValueOperators.filter] * - [`$firstN`][ArrayValueOperators.take] * - [`$lastN`][ArrayValueOperators.takeLast] + * - [`$sortArray`][ArrayValueOperators.sortedBy] * * Trigonometric operators and angle management: * - [`$acos`][TrigonometryValueOperators.acos] diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt index 14628b3e..4b6ccb37 100644 --- a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt @@ -17,13 +17,19 @@ package opensavvy.ktmongo.dsl.aggregation.operators import opensavvy.ktmongo.bson.BsonContext +import opensavvy.ktmongo.bson.BsonFieldWriter import opensavvy.ktmongo.bson.BsonValueWriter +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.Value import opensavvy.ktmongo.dsl.aggregation.ValueDsl +import opensavvy.ktmongo.dsl.expr.common.AbstractCompoundExpression +import opensavvy.ktmongo.dsl.expr.common.AbstractExpression +import opensavvy.ktmongo.dsl.options.common.SortOptionDsl import opensavvy.ktmongo.dsl.path.Field +import opensavvy.ktmongo.dsl.path.Path import kotlin.reflect.KProperty1 /** @@ -541,5 +547,222 @@ interface ArrayValueOperators : ValueOperators { } // 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/) + */ + @OptIn(LowLevelApi::class) + @KtMongoDsl + fun Value>.sortedBy( + order: SortOptionDsl.() -> Unit, + ): Value> = + SortValueOperator( + input = this, + sortOrder = SortOptionDslExpression(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/) + */ + @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/) + */ + @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/) + */ + @KtMongoDsl + fun Collection.sortedBy( + order: SortOptionDsl.() -> Unit, + ): Value> = + of(this).sortedBy(order) + + @LowLevelApi + private class SortOptionDslExpression( + context: BsonContext, + ) : AbstractCompoundExpression(context), SortOptionDsl { + + @OptIn(DangerousMongoApi::class) + override fun ascending(field: Field) { + accept(SortExpression(field.path, 1, context)) + } + + @OptIn(DangerousMongoApi::class) + override fun descending(field: Field) { + accept(SortExpression(field.path, -1, context)) + } + + @LowLevelApi + private class SortExpression( + val path: Path, + val value: Int, + context: BsonContext, + ) : AbstractExpression(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@SortOptionDslExpression.freeze() + } + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + this@SortOptionDslExpression.writeTo(this) + } + } + } + } + + @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/commonTest/kotlin/aggregation/AggregationTestUtils.kt b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt index 8d5e225a..7723303e 100644 --- a/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt +++ b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt @@ -38,6 +38,7 @@ val project = "\$project" val filter = "\$filter" val firstN = "\$firstN" val lastN = "\$lastN" +val sortArray = "\$sortArray" @OptIn(LowLevelApi::class) class TestPipeline( diff --git a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt index c5dffe8c..d94927ac 100644 --- a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt +++ b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt @@ -22,12 +22,19 @@ import opensavvy.prepared.runner.kotest.PreparedSpec class ArrayValueOperatorsTest : PreparedSpec({ + class User( + val name: String, + val age: Int, + ) + class Target( val numbers: List, + val users: List, val results: List ) val numbers = "\$numbers" + val users = "\$users" val arrayThis = "$\$this" suite(filter) { @@ -173,4 +180,34 @@ class ArrayValueOperatorsTest : PreparedSpec({ } } + suite(sortArray) { + test("Sort by field") { + TestPipeline() + .set { + Target::results set Target::users + .sortedBy { + ascending(User::name) + descending(User::age) + } + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$sortArray": { + "input": "$users", + "sortBy": { + "name": 1, + "age": -1 + } + } + } + } + } + ] + """.trimIndent()) + } + } + }) -- 2.51.2 From a607fcce977a62f706e54381b07011dba75ad1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 1 Mar 2025 17:10:17 +0100 Subject: [PATCH 5/7] feat(dsl): Add the $sortArray aggregation operator (sort by elements) --- .../operators/ArrayValueOperators.kt | 297 ++++++++++++++++++ .../operators/ArrayValueOperatorsTest.kt | 44 +++ 2 files changed, 341 insertions(+) diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt index 4b6ccb37..fb835332 100644 --- a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt @@ -576,6 +576,9 @@ interface ArrayValueOperators : ValueOperators { * ### 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 @@ -615,6 +618,9 @@ interface ArrayValueOperators : ValueOperators { * ### 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( @@ -649,6 +655,9 @@ interface ArrayValueOperators : ValueOperators { * ### 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( @@ -683,6 +692,9 @@ interface ArrayValueOperators : ValueOperators { * ### 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( @@ -737,6 +749,291 @@ interface ArrayValueOperators : ValueOperators { } } + /** + * 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>, diff --git a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt index d94927ac..e5cdc3ca 100644 --- a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt +++ b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt @@ -208,6 +208,50 @@ class ArrayValueOperatorsTest : PreparedSpec({ ] """.trimIndent()) } + + test("Sort by elements (ascending)") { + TestPipeline() + .set { + Target::results set Target::numbers + .sorted() + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$sortArray": { + "input": "$numbers", + "sortBy": 1 + } + } + } + } + ] + """.trimIndent()) + } + + test("Sort by elements (descending)") { + TestPipeline() + .set { + Target::results set Target::numbers + .sortedDescending() + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$sortArray": { + "input": "$numbers", + "sortBy": -1 + } + } + } + } + ] + """.trimIndent()) + } } }) -- 2.51.2 From 5c90309656fb24d7d82171a1a2c4ae27289c0ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 1 Mar 2025 17:45:11 +0100 Subject: [PATCH 6/7] feat(dsl): Add the $map aggregation operator --- .../commonMain/kotlin/aggregation/ValueDsl.kt | 1 + .../operators/ArrayValueOperators.kt | 166 ++++++++++++++++++ .../aggregation/AggregationTestUtils.kt | 1 + .../operators/ArrayValueOperatorsTest.kt | 100 ++++++++++- 4 files changed, 266 insertions(+), 2 deletions(-) diff --git a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt index 81bcd013..95cd684c 100644 --- a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt +++ b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt @@ -98,6 +98,7 @@ import opensavvy.ktmongo.dsl.path.Field * - [`$filter`][ArrayValueOperators.filter] * - [`$firstN`][ArrayValueOperators.take] * - [`$lastN`][ArrayValueOperators.takeLast] + * - [`$map`][ArrayValueOperators.map] * - [`$sortArray`][ArrayValueOperators.sortedBy] * * Trigonometric operators and angle management: diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt index fb835332..1a32cd02 100644 --- a/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ArrayValueOperators.kt @@ -546,6 +546,172 @@ interface ArrayValueOperators : ValueOperators { } } + // 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: ValueDsl.(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: ValueDsl.(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: ValueDsl.(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: ValueDsl.(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 diff --git a/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt index 7723303e..7946ad2f 100644 --- a/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt +++ b/dsl/src/commonTest/kotlin/aggregation/AggregationTestUtils.kt @@ -36,6 +36,7 @@ val sort = "\$sort" val unset = "\$unset" val project = "\$project" val filter = "\$filter" +val map = "\$map" val firstN = "\$firstN" val lastN = "\$lastN" val sortArray = "\$sortArray" diff --git a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt index e5cdc3ca..7669298b 100644 --- a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt +++ b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt @@ -17,6 +17,7 @@ package opensavvy.ktmongo.dsl.aggregation.operators.aggregation.operators import opensavvy.ktmongo.dsl.aggregation.* +import opensavvy.ktmongo.dsl.expr.filter.eq import opensavvy.ktmongo.dsl.expr.filter.gt import opensavvy.prepared.runner.kotest.PreparedSpec @@ -36,6 +37,7 @@ class ArrayValueOperatorsTest : PreparedSpec({ val numbers = "\$numbers" val users = "\$users" val arrayThis = "$\$this" + val foo = "$\$foo" suite(filter) { test("Usage with a list of integers") { @@ -100,8 +102,6 @@ class ArrayValueOperatorsTest : PreparedSpec({ } test("Usage with another variable name") { - val foo = "$\$foo" - TestPipeline() .set { Target::results set Target::numbers @@ -132,6 +132,102 @@ class ArrayValueOperatorsTest : PreparedSpec({ } } + suite(map) { + test("Usage with a list of integers") { + TestPipeline() + .set { + Target::results set Target::numbers + .map { + it + of(4) + } + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$map": { + "input": "$numbers", + "as": "this", + "in": { + "$add": [ + "$arrayThis", + {"$literal": 4} + ] + } + } + } + } + } + ] + """.trimIndent()) + } + + test("Usage with a variable name") { + TestPipeline() + .set { + Target::results set Target::numbers + .map(variableName = "foo") { + it + of(4) + } + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$map": { + "input": "$numbers", + "as": "foo", + "in": { + "$add": [ + "$foo", + {"$literal": 4} + ] + } + } + } + } + } + ] + """.trimIndent()) + } + + test("Usage with a type conversion") { + TestPipeline() + .set { + Target::results set Target::numbers + .map { + it eq of(4) + } + .also { + @Suppress("UnusedVariable", "unused") + val foo: Value> = it // Ensure that the type doesn't change + } + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$map": { + "input": "$numbers", + "as": "this", + "in": { + "$eq": [ + "$arrayThis", + {"$literal": 4} + ] + } + } + } + } + } + ] + """.trimIndent()) + } + } + suite(firstN) { test("Usage with a list of integers") { TestPipeline() -- 2.51.2 From 28946504868b87074cb0d9ef13d2f80f7051e368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 1 Mar 2025 18:02:20 +0100 Subject: [PATCH 7/7] feat(dsl): Add the $getField aggregation operator --- .../commonMain/kotlin/aggregation/ValueDsl.kt | 3 + .../aggregation/operators/ValueOperators.kt | 80 +++++++++++++++++++ .../operators/ArrayValueOperatorsTest.kt | 35 ++++++++ .../kotlin/expr/filter/FilterUtils.kt | 1 + 4 files changed, 119 insertions(+) diff --git a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt index 95cd684c..c8e4b12f 100644 --- a/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt +++ b/dsl/src/commonMain/kotlin/aggregation/ValueDsl.kt @@ -101,6 +101,9 @@ import opensavvy.ktmongo.dsl.path.Field * - [`$map`][ArrayValueOperators.map] * - [`$sortArray`][ArrayValueOperators.sortedBy] * + * Document operators: + * - [`$getField`][ValueOperators.div] + * * Trigonometric operators and angle management: * - [`$acos`][TrigonometryValueOperators.acos] * - [`$acosh`][TrigonometryValueOperators.acosh] diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt index 52bac344..b0fdff6c 100644 --- a/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt @@ -24,6 +24,7 @@ import opensavvy.ktmongo.dsl.aggregation.Value import opensavvy.ktmongo.dsl.aggregation.ValueDsl import opensavvy.ktmongo.dsl.path.Field import opensavvy.ktmongo.dsl.path.FieldDsl +import opensavvy.ktmongo.dsl.path.Path import kotlin.reflect.KProperty1 /** @@ -100,6 +101,63 @@ interface ValueOperators : FieldDsl { fun of(value: Result): Value = LiteralValue(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) @@ -127,3 +185,25 @@ private class LiteralValue( } } } + +@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/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt index 7669298b..e8adcc9b 100644 --- a/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt +++ b/dsl/src/commonTest/kotlin/aggregation/operators/aggregation/operators/ArrayValueOperatorsTest.kt @@ -18,6 +18,7 @@ package opensavvy.ktmongo.dsl.aggregation.operators.aggregation.operators import opensavvy.ktmongo.dsl.aggregation.* import opensavvy.ktmongo.dsl.expr.filter.eq +import opensavvy.ktmongo.dsl.expr.filter.getField import opensavvy.ktmongo.dsl.expr.filter.gt import opensavvy.prepared.runner.kotest.PreparedSpec @@ -226,6 +227,40 @@ class ArrayValueOperatorsTest : PreparedSpec({ ] """.trimIndent()) } + + test("Usage with a child field") { + TestPipeline() + .set { + Target::results set Target::users + .map { + it / User::name + } + .also { + @Suppress("UnusedVariable", "unused") + val foo: Value> = it // Ensure that the type doesn't change + } + } + .shouldBeBson(""" + [ + { + "$set": { + "results": { + "$map": { + "input": "$users", + "as": "this", + "in": { + "$getField": { + "input": "$arrayThis", + "field": "name" + } + } + } + } + } + } + ] + """.trimIndent()) + } } suite(firstN) { diff --git a/dsl/src/commonTest/kotlin/expr/filter/FilterUtils.kt b/dsl/src/commonTest/kotlin/expr/filter/FilterUtils.kt index 5105da1a..44c28b98 100644 --- a/dsl/src/commonTest/kotlin/expr/filter/FilterUtils.kt +++ b/dsl/src/commonTest/kotlin/expr/filter/FilterUtils.kt @@ -38,6 +38,7 @@ val all = "\$all" val oid = "\$oid" val elemMatch = "\$elemMatch" val expr = "\$expr" +val getField = "\$getField" class Pet( val name: String, -- 2.51.2