From 5d164a713abacd8744ede6b17dcf656025c9631e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Aug 2025 10:14:49 +0200 Subject: [PATCH 1/4] feat(dsl): Support $push (update) --- .../commonMain/kotlin/query/UpdateQuery.kt | 153 +++++++++++++++++- .../kotlin/query/UpdateQueryImpl.kt | 44 ++++- .../kotlin/query/update/FieldUpdateTest.kt | 63 ++++++++ 3 files changed, 258 insertions(+), 2 deletions(-) diff --git a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt index 54aa10e..511f340 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt @@ -63,6 +63,7 @@ import kotlin.time.Instant * - [`$`][selected] * - [`$[]`][all] * - [`$addToSet`][addToSet] + * - [`$push`][push] * * Time management: * - [`$currentDate`][setToCurrentDate] @@ -526,7 +527,7 @@ interface UpdateQuery : CompoundBsonNode, FieldDsl { this.field.unset() } - // endregion + // endregion // region $min & $max /** @@ -1196,6 +1197,156 @@ interface UpdateQuery : CompoundBsonNode, FieldDsl { } // endregion + // region $push + + /** + * Adds [value] at the end of the array. + * + * Unlike [addToSet], this operator always adds the value, even if it's already present in the array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores push 100 + * } + * ) + * ``` + * + * This will add `100` to the user's scores, even if it's already present. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/v7.0/reference/operator/update/push/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> Field>.push(value: V) + + /** + * Adds [value] at the end of the array. + * + * Unlike [addToSet], this operator always adds the value, even if it's already present in the array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores push 100 + * } + * ) + * ``` + * + * This will add `100` to the user's scores, even if it's already present. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/v7.0/reference/operator/update/push/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> KProperty1>.push(value: V) { + this.field.push(value) + } + + /** + * Adds multiple [values] at the end of the array. + * + * Unlike [addToSet], this operator always adds all values, even if they're already present in the array. + * + * This is a convenience function for calling [push] multiple times. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores push listOf(100, 200) + * } + * ) + * ``` + * + * This will add `100` and `200` to the user's scores, even if they're already present. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/v7.0/reference/operator/update/push/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> Field>.push(values: Iterable) { + for (value in values) + this push value + } + + /** + * Adds multiple [values] at the end of the array. + * + * Unlike [addToSet], this operator always adds all values, even if they're already present in the array. + * + * This is a convenience function for calling [push] multiple times. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores push listOf(100, 200) + * } + * ) + * ``` + * + * This will add `100` and `200` to the user's scores, even if they're already present. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/v7.0/reference/operator/update/push/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> KProperty1>.push(values: Iterable) { + this.field push values + } + + // endregion } diff --git a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt index 9adcebb..3d6984f 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -348,7 +348,7 @@ private class UpdateQueryImpl( } // endregion - // region $setOnInsert + // region $addToSet @OptIn(DangerousMongoApi::class, LowLevelApi::class) override fun Field>.addToSet(value: V) { @@ -386,6 +386,45 @@ private class UpdateQueryImpl( } } + // endregion + // region $push + + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + override fun Field>.push(value: V) { + accept(PushBsonNode(listOf(this.path to value), context)) + } + + @LowLevelApi + private class PushBsonNode( + val mappings: List>, + context: BsonContext, + ) : UpdateBsonNodeNode(context) { + + override fun simplify() = + this.takeUnless { mappings.isEmpty() } + + override fun write(writer: BsonFieldWriter) = with(writer) { + val groupedMappings = mappings.groupBy( + keySelector = { it.first }, + valueTransform = { it.second }, + ) + + writeDocument("\$push") { + for ((field, values) in groupedMappings) { + if (values.size == 1) + writeObjectSafe(field.toString(), values.single()) + else + writeDocument(field.toString()) { + writeArray("\$each") { + for (value in values) + writeObjectSafe(value) + } + } + } + } + } + } + // endregion companion object { @@ -418,6 +457,9 @@ private class UpdateQueryImpl( OperatorCombinator(AddToSetBsonNode::class) { sources, context -> AddToSetBsonNode(sources.flatMap { it.mappings }, context) }, + OperatorCombinator(PushBsonNode::class) { sources, context -> + PushBsonNode(sources.flatMap { it.mappings }, context) + }, OperatorCombinator(CurrentDateBsonNode::class) { sources, context -> CurrentDateBsonNode(sources.flatMap { it.mappings }, context) }, diff --git a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt index e11eb73..2985bd6 100644 --- a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt +++ b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt @@ -394,6 +394,69 @@ val FieldUpdateTest by preparedSuite { """.trimIndent() } } + + suite($$"$push") { + test("Add a single field") { + update { + User::tokens push "123" + } shouldBeBson $$""" + { + "$push": { + "tokens": "123" + } + } + """.trimIndent() + } + + test("Add multiple fields") { + update { + User::tokens push "123" + User::scores push 1 + } shouldBeBson $$""" + { + "$push": { + "tokens": "123", + "scores": 1 + } + } + """.trimIndent() + } + + test("Add multiple values to the same field") { + update { + User::tokens push "123" + User::tokens push "456" + } shouldBeBson $$""" + { + "$push": { + "tokens": { + "$each": [ + "123", + "456" + ] + } + } + } + """.trimIndent() + } + + test("Add multiple values to the same field using a list") { + update { + User::tokens push listOf("123", "456") + } shouldBeBson $$""" + { + "$push": { + "tokens": { + "$each": [ + "123", + "456" + ] + } + } + } + """.trimIndent() + } + } } suite($$"$currentDate") { -- GitLab From 345cee737459caca0ada61b81d2711d9406631e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Aug 2025 14:07:17 +0200 Subject: [PATCH 2/4] feat(dsl): Support $push's $slice parameter --- .../commonMain/kotlin/query/UpdateQuery.kt | 214 ++++++++++++++++++ .../kotlin/query/UpdateQueryImpl.kt | 59 +++++ .../kotlin/query/update/FieldUpdateTest.kt | 38 ++++ 3 files changed, 311 insertions(+) diff --git a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt index 511f340..fc196cb 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt @@ -21,11 +21,13 @@ import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.KtMongoDsl import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.path.* +import opensavvy.ktmongo.dsl.tree.BsonNode import opensavvy.ktmongo.dsl.tree.CompoundBsonNode import kotlin.reflect.KProperty1 import kotlin.time.ExperimentalTime import kotlin.time.Instant + /** * DSL for MongoDB operators that are used to update existing values (does *not* include aggregation operators). * @@ -1346,8 +1348,220 @@ interface UpdateQuery : CompoundBsonNode, FieldDsl { this.field push values } + /** + * Adds values to the end of the array with advanced options. + * + * This method allows using MongoDB's advanced `$push` operators like `$each` and `$slice`. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val tokens: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::tokens push { + * each("123", "456") + * slice(3) + * } + * } + * ) + * ``` + * + * This will add `"123"` and `"456"` to the user's tokens and keep only the last 3 elements. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/v7.0/reference/operator/update/push/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> Field>.push(builder: PushBuilder.() -> Unit) + + /** + * Adds values to the end of the array with advanced options. + * + * This method allows using MongoDB's advanced `$push` operators like `$each` and `$slice`. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val tokens: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::tokens push { + * each("123", "456") + * slice(3) + * } + * } + * ) + * ``` + * + * This will add `"123"` and `"456"` to the user's tokens and keep only the last 3 elements. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/v7.0/reference/operator/update/push/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + infix fun <@kotlin.internal.OnlyInputTypes V> KProperty1>.push(builder: PushBuilder.() -> Unit) { + this.field push builder + } + // endregion + /** + * DSL builder for advanced `$push` operations. + * + * See [push]. + */ + @KtMongoDsl + interface PushBuilder : BsonNode { + + /** + * Specifies the values to push to the array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tokens: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::tokens push { + * each("123", "456") + * } + * } + * ) + * ``` + * + * You can also call [push] multiple times: + * + * ```kotlin + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::tokens push "123" + * User::tokens push "456" + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/push/) + */ + @KtMongoDsl + fun each(vararg values: V) { + each(values.asIterable()) + } + + /** + * Specifies the values to push to the array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tokens: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::tokens push { + * each(listOf("123", "456")) + * } + * } + * ) + * ``` + * + * You can also call [push] multiple times: + * + * ```kotlin + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::tokens push "123" + * User::tokens push "456" + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/push/) + */ + @KtMongoDsl + fun each(values: Iterable) + + /** + * Limits the number of array elements after the push operation. + * + * If [count] is **positive**, after adding the elements to the array, + * only the **first** [count] elements are kept, and all additional elements are discarded. + * + * If [count] is **negative**, after adding the elements to the array, + * only the **last** [count] elements are kept, and prior elements are discarded. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tokens: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::tokens push { + * each("123", "456") + * slice(3) // Keep only the first 3 elements + * } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/slice) + */ + @KtMongoDsl + fun slice(count: Int) + } + } /** diff --git a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt index 3d6984f..2d05ce6 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -389,11 +389,45 @@ private class UpdateQueryImpl( // endregion // region $push + @LowLevelApi + private class PushBuilderImpl( + context: BsonContext, + ) : AbstractBsonNode(context), UpdateQuery.PushBuilder { + var values: List = emptyList() + var sliceValue: Int? = null + + override fun each(values: Iterable) { + this.values = values.toList() + } + + override fun slice(count: Int) { + this.sliceValue = count + } + + override fun write(writer: BsonFieldWriter) { + with(writer) { + writeArray($$"$each") { + for (value in values) + writeObjectSafe(value) + } + sliceValue?.let { sliceValue -> + writeInt32($$"$slice", sliceValue) + } + } + } + } + @OptIn(DangerousMongoApi::class, LowLevelApi::class) override fun Field>.push(value: V) { accept(PushBsonNode(listOf(this.path to value), context)) } + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + override fun Field>.push(builder: UpdateQuery.PushBuilder.() -> Unit) { + val pushBuilder = PushBuilderImpl(context).apply(builder) + accept(AdvancedPushBsonNode(this.path, pushBuilder, context)) + } + @LowLevelApi private class PushBsonNode( val mappings: List>, @@ -425,6 +459,31 @@ private class UpdateQueryImpl( } } + @LowLevelApi + private class AdvancedPushBsonNode( + val path: Path, + val pushBuilder: PushBuilderImpl, + context: BsonContext, + ) : UpdateBsonNodeNode(context) { + + override fun simplify() = + this.takeUnless { pushBuilder.values.isEmpty() && pushBuilder.sliceValue == null } + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument("\$push") { + writeDocument(path.toString()) { + writeArray("\$each") { + for (value in pushBuilder.values) + writeObjectSafe(value) + } + pushBuilder.sliceValue?.let { sliceValue -> + writeInt32("\$slice", sliceValue) + } + } + } + } + } + // endregion companion object { diff --git a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt index 2985bd6..4b6f505 100644 --- a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt +++ b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt @@ -456,6 +456,44 @@ val FieldUpdateTest by preparedSuite { } """.trimIndent() } + + test("Add values with slice") { + update { + User::tokens push { + each("123", "456") + slice(3) + } + } shouldBeBson $$""" + { + "$push": { + "tokens": { + "$each": [ + "123", + "456" + ], + "$slice": 3 + } + } + } + """.trimIndent() + } + + test("Use slice without each") { + update { + User::tokens push { + slice(3) + } + } shouldBeBson $$""" + { + "$push": { + "tokens": { + "$each": [], + "$slice": 3 + } + } + } + """.trimIndent() + } } } -- GitLab From f86f3e6fa4741a40b8521b4d9fffd001f70f1548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Aug 2025 16:21:43 +0200 Subject: [PATCH 3/4] feat(dsl): Support $push's $position parameter --- .../commonMain/kotlin/query/UpdateQuery.kt | 43 +++++++ .../kotlin/query/UpdateQueryImpl.kt | 13 ++- .../kotlin/query/update/FieldUpdateTest.kt | 105 ++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) diff --git a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt index fc196cb..ddeadcf 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt @@ -1560,6 +1560,49 @@ interface UpdateQuery : CompoundBsonNode, FieldDsl { */ @KtMongoDsl fun slice(count: Int) + + /** + * Specifies the location in the array at which the `$push` operator inserts elements. + * + * Without the `$position` modifier, the `$push` operator inserts elements to the end of the array. + * + * A **non-negative** number corresponds to the position in the array, starting from the beginning of the array. + * If the value is greater or equal to the length of the array, the `$position` modifier has no effect + * and `$push` adds elements to the end of the array. + * + * A **negative** number corresponds to the position in the array, counting from (but not including) + * the last element of the array. For example, -1 indicates the position just before the last element + * in the array. If you specify multiple elements in the `$each` array, the last added element is in + * the specified position from the end. If the absolute value is greater than or equal to the length + * of the array, the `$push` adds elements to the beginning of the array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val scores: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores push { + * each(50, 60, 70) + * position(0) // Insert at the beginning + * } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/position/) + */ + @KtMongoDsl + fun position(index: Int) } } diff --git a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt index 2d05ce6..eab1ca8 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -395,6 +395,7 @@ private class UpdateQueryImpl( ) : AbstractBsonNode(context), UpdateQuery.PushBuilder { var values: List = emptyList() var sliceValue: Int? = null + var positionValue: Int? = null override fun each(values: Iterable) { this.values = values.toList() @@ -404,12 +405,19 @@ private class UpdateQueryImpl( this.sliceValue = count } + override fun position(index: Int) { + this.positionValue = index + } + override fun write(writer: BsonFieldWriter) { with(writer) { writeArray($$"$each") { for (value in values) writeObjectSafe(value) } + positionValue?.let { positionValue -> + writeInt32($$"$position", positionValue) + } sliceValue?.let { sliceValue -> writeInt32($$"$slice", sliceValue) } @@ -467,7 +475,7 @@ private class UpdateQueryImpl( ) : UpdateBsonNodeNode(context) { override fun simplify() = - this.takeUnless { pushBuilder.values.isEmpty() && pushBuilder.sliceValue == null } + this.takeUnless { pushBuilder.values.isEmpty() && pushBuilder.sliceValue == null && pushBuilder.positionValue == null } override fun write(writer: BsonFieldWriter) = with(writer) { writeDocument("\$push") { @@ -476,6 +484,9 @@ private class UpdateQueryImpl( for (value in pushBuilder.values) writeObjectSafe(value) } + pushBuilder.positionValue?.let { positionValue -> + writeInt32("\$position", positionValue) + } pushBuilder.sliceValue?.let { sliceValue -> writeInt32("\$slice", sliceValue) } diff --git a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt index 4b6f505..7fbcce5 100644 --- a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt +++ b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt @@ -494,6 +494,111 @@ val FieldUpdateTest by preparedSuite { } """.trimIndent() } + + test("Add values at the beginning with position 0") { + update { + User::scores push { + each(50, 60, 70) + position(0) + } + } shouldBeBson $$""" + { + "$push": { + "scores": { + "$each": [ + 50, + 60, + 70 + ], + "$position": 0 + } + } + } + """.trimIndent() + } + + test("Add values at middle position") { + update { + User::scores push { + each(20, 30) + position(2) + } + } shouldBeBson $$""" + { + "$push": { + "scores": { + "$each": [ + 20, + 30 + ], + "$position": 2 + } + } + } + """.trimIndent() + } + + test("Add values with negative position") { + update { + User::scores push { + each(90, 80) + position(-2) + } + } shouldBeBson $$""" + { + "$push": { + "scores": { + "$each": [ + 90, + 80 + ], + "$position": -2 + } + } + } + """.trimIndent() + } + + test("Add values with position, each, and slice") { + update { + User::scores push { + each(10, 20, 30) + position(1) + slice(5) + } + } shouldBeBson $$""" + { + "$push": { + "scores": { + "$each": [ + 10, + 20, + 30 + ], + "$position": 1, + "$slice": 5 + } + } + } + """.trimIndent() + } + + test("Use position without each") { + update { + User::tokens push { + position(0) + } + } shouldBeBson $$""" + { + "$push": { + "tokens": { + "$each": [], + "$position": 0 + } + } + } + """.trimIndent() + } } } -- GitLab From 91955649e5d3ebcf3c18879a8e8a491729035bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Wed, 13 Aug 2025 15:33:28 +0200 Subject: [PATCH 4/4] feat(dsl): Support $push's $sort parameter --- .../commonMain/kotlin/query/UpdateQuery.kt | 122 +++++++++++++ .../kotlin/query/UpdateQueryImpl.kt | 64 ++++++- .../kotlin/query/update/FieldUpdateTest.kt | 167 ++++++++++++++++++ 3 files changed, 352 insertions(+), 1 deletion(-) diff --git a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt index ddeadcf..11dfc7b 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt @@ -20,6 +20,7 @@ import opensavvy.ktmongo.bson.types.Timestamp import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.KtMongoDsl import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.options.SortOptionDsl import opensavvy.ktmongo.dsl.path.* import opensavvy.ktmongo.dsl.tree.BsonNode import opensavvy.ktmongo.dsl.tree.CompoundBsonNode @@ -1603,6 +1604,127 @@ interface UpdateQuery : CompoundBsonNode, FieldDsl { */ @KtMongoDsl fun position(index: Int) + + /** + * Orders the elements of an array during a `$push` operation. + * + * ### Example + * + * ```kotlin + * class Quiz( + * val id: Int, + * val score: Int, + * ) + * + * class User( + * val name: String, + * val quizzes: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::quizzes push { + * each(Quiz(3, 8), Quiz(4, 7), Quiz(5, 6)) + * sort { + * ascending(Quiz::score) + * } + * } + * } + * ) + * ``` + * + * To sort based on the natural order of the array elements themselves: + * ```kotlin + * class User( + * val name: String, + * val scores: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores push { + * each(12, 34) + * sort { ascending() } + * } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/sort/) + */ + @KtMongoDsl + fun sort(block: PushSortDsl.() -> Unit) + } + + /** + * DSL for sorting elements during a `$push` operation. + * + * See [push] and [PushBuilder.sort]. + */ + @KtMongoDsl + interface PushSortDsl : SortOptionDsl { + + /** + * Sort array elements in ascending order (for simple values, not documents). + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::tests push { + * each(40, 60) + * sort { ascending() } + * } + * } + * ) + * ``` + */ + @KtMongoDsl + fun ascending() + + /** + * Sort array elements in descending order (for simple values, not documents). + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * collection.updateOne( + * filter = { + * User::name eq "Bob" + * }, + * update = { + * User::tests push { + * each(40, 60) + * sort { descending() } + * } + * } + * ) + * ``` + */ + @KtMongoDsl + fun descending() } } diff --git a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt index eab1ca8..3ceccd6 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -389,6 +389,62 @@ private class UpdateQueryImpl( // endregion // region $push + @LowLevelApi + private class PushSortOptionDslBsonNode( + context: BsonContext, + ) : AbstractCompoundBsonNode(context), UpdateQuery.PushSortDsl { + var isSimpleValue = false + var simpleValue = 1 + + @OptIn(DangerousMongoApi::class) + override fun ascending(field: Field) { + accept(PushSortBsonNode(field.path, 1, context)) + } + + @OptIn(DangerousMongoApi::class) + override fun descending(field: Field) { + accept(PushSortBsonNode(field.path, -1, context)) + } + + /** + * Sort array elements in ascending order (for simple values, not documents). + */ + override fun ascending() { + isSimpleValue = true + simpleValue = 1 + } + + /** + * Sort array elements in descending order (for simple values, not documents). + */ + override fun descending() { + isSimpleValue = true + simpleValue = -1 + } + + fun writeSort(writer: BsonFieldWriter) = with(writer) { + if (isSimpleValue) { + writeInt32("\$sort", simpleValue) + } else { + writeDocument("\$sort") { + writeTo(this) + } + } + } + + @LowLevelApi + private class PushSortBsonNode( + val path: Path, + val value: Int, + context: BsonContext, + ) : AbstractBsonNode(context) { + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeInt32(path.toString(), value) + } + } + } + @LowLevelApi private class PushBuilderImpl( context: BsonContext, @@ -396,6 +452,7 @@ private class UpdateQueryImpl( var values: List = emptyList() var sliceValue: Int? = null var positionValue: Int? = null + var sortNode: PushSortOptionDslBsonNode? = null override fun each(values: Iterable) { this.values = values.toList() @@ -409,6 +466,10 @@ private class UpdateQueryImpl( this.positionValue = index } + override fun sort(block: UpdateQuery.PushSortDsl.() -> Unit) { + this.sortNode = PushSortOptionDslBsonNode(context).apply(block) + } + override fun write(writer: BsonFieldWriter) { with(writer) { writeArray($$"$each") { @@ -475,7 +536,7 @@ private class UpdateQueryImpl( ) : UpdateBsonNodeNode(context) { override fun simplify() = - this.takeUnless { pushBuilder.values.isEmpty() && pushBuilder.sliceValue == null && pushBuilder.positionValue == null } + this.takeUnless { pushBuilder.values.isEmpty() && pushBuilder.sliceValue == null && pushBuilder.positionValue == null && pushBuilder.sortNode == null } override fun write(writer: BsonFieldWriter) = with(writer) { writeDocument("\$push") { @@ -490,6 +551,7 @@ private class UpdateQueryImpl( pushBuilder.sliceValue?.let { sliceValue -> writeInt32("\$slice", sliceValue) } + pushBuilder.sortNode?.writeSort(this) } } } diff --git a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt index 7fbcce5..38846d0 100644 --- a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt +++ b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt @@ -599,6 +599,173 @@ val FieldUpdateTest by preparedSuite { } """.trimIndent() } + + test("Sort simple values in ascending order") { + update { + User::scores push { + each(40, 60) + sort { + ascending() + } + } + } shouldBeBson $$""" + { + "$push": { + "scores": { + "$each": [ + 40, + 60 + ], + "$sort": 1 + } + } + } + """.trimIndent() + } + + test("Sort simple values in descending order") { + update { + User::scores push { + each(40, 60) + sort { + descending() + } + } + } shouldBeBson $$""" + { + "$push": { + "scores": { + "$each": [ + 40, + 60 + ], + "$sort": -1 + } + } + } + """.trimIndent() + } + + test("Sort documents by field in ascending order") { + update { + User::friends push { + each(Friend("1", "Alice", 1000.0f), Friend("2", "Bob", 2000.0f)) + sort { ascending(Friend::name) } + } + } shouldBeBson $$""" + { + "$push": { + "friends": { + "$each": [ + { + "id": "1", + "name": "Alice", + "money": 1000.0 + }, + { + "id": "2", + "name": "Bob", + "money": 2000.0 + } + ], + "$sort": { + "name": 1 + } + } + } + } + """.trimIndent() + } + + test("Sort documents by field in descending order") { + update { + User::friends push { + each(Friend("1", "Alice", 1000.0f), Friend("2", "Bob", 2000.0f)) + sort { descending(Friend::money) } + } + } shouldBeBson $$""" + { + "$push": { + "friends": { + "$each": [ + { + "id": "1", + "name": "Alice", + "money": 1000.0 + }, + { + "id": "2", + "name": "Bob", + "money": 2000.0 + } + ], + "$sort": { + "money": -1 + } + } + } + } + """.trimIndent() + } + + test("Sort with empty array (sort only)") { + update { + User::scores push { + sort { + descending() + } + } + } shouldBeBson $$""" + { + "$push": { + "scores": { + "$each": [], + "$sort": -1 + } + } + } + """.trimIndent() + } + + test("Sort combined with slice and position") { + update { + User::friends push { + each(Friend("1", "Alice", 1000.0f), Friend("2", "Bob", 2000.0f), Friend("3", "Charlie", 500.0f)) + sort { descending(Friend::name) } + slice(2) + position(0) + } + } shouldBeBson $$""" + { + "$push": { + "friends": { + "$each": [ + { + "id": "1", + "name": "Alice", + "money": 1000.0 + }, + { + "id": "2", + "name": "Bob", + "money": 2000.0 + }, + { + "id": "3", + "name": "Charlie", + "money": 500.0 + } + ], + "$position": 0, + "$slice": 2, + "$sort": { + "name": -1 + } + } + } + } + """.trimIndent() + } } } -- GitLab