From 8366d4a01ba85eae76e9357d8f478f97bca393b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Wed, 27 May 2026 23:54:12 +0200 Subject: [PATCH 1/3] feat(dsl): Add $pop (update) --- .../commonMain/kotlin/query/UpdateQuery.kt | 62 +++++++++ .../kotlin/query/UpdateQueryImpl.kt | 34 +++++ .../commonMain/kotlin/query/UpdateQuery.kt | 123 ++++++++++++++++++ .../kotlin/query/UpdateQueryImpl.kt | 34 +++++ .../kotlin/query/update/FieldUpdateTest.kt | 42 ++++++ 5 files changed, 295 insertions(+) diff --git a/dsl-template/src/commonMain/kotlin/query/UpdateQuery.kt b/dsl-template/src/commonMain/kotlin/query/UpdateQuery.kt index 514f08ff..a32512c3 100644 --- a/dsl-template/src/commonMain/kotlin/query/UpdateQuery.kt +++ b/dsl-template/src/commonMain/kotlin/query/UpdateQuery.kt @@ -68,6 +68,8 @@ import kotlin.time.Instant * - [`$[]`][all] * - [`$[]`][filter] * - [`$addToSet`][addToSet] + * - [`$pop`][popLast] (last) + * - [`$pop`][popFirst] (first) * - [`$push`][push] * * Time management: @@ -881,6 +883,66 @@ interface UpdateQuery : CompoundBsonNode, FieldDsl { this.addEachToSet(values, typeOf()) } + // endregion + // region $pop + + /** + * Removes the last element in the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * fiilter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores.popLast() + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pop/) + */ + @KtMongoDsl + fun Field>.popLast() + + /** + * Removes the first element in the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * fiilter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores.popFirst() + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pop/) + */ + fun Field>.popFirst() + // endregion // region $push diff --git a/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt index 132617e0..a998e21e 100644 --- a/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -397,6 +397,37 @@ private class UpdateQueryImpl( } } + // endregion + // region $pop + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun Field>.popLast() { + accept(PopBsonNode(listOf(this.path to 1), context)) + } + + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + override fun Field>.popFirst() { + accept(PopBsonNode(listOf(this.path to -1), context)) + } + + @LowLevelApi + private class PopBsonNode( + val mappings: List>, + context: BsonContext, + ) : UpdateBsonNodeNode(context) { + + override fun simplify() = + this.takeUnless { mappings.isEmpty() } + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument($$"$pop") { + for ((field, direction) in mappings) { + writeInt32(field.toString(), direction) + } + } + } + } + // endregion // region $push @@ -654,6 +685,9 @@ private class UpdateQueryImpl( OperatorCombinator(AddToSetBsonNode::class) { sources, context -> AddToSetBsonNode(sources.flatMap { it.mappings }, context) }, + OperatorCombinator(PopBsonNode::class) { sources, context -> + PopBsonNode(sources.flatMap { it.mappings }, context) + }, OperatorCombinator(PushBsonNode::class) { sources, context -> PushBsonNode(sources.flatMap { it.mappings }, context) }, diff --git a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt index a9710f64..82ca035b 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt @@ -71,6 +71,8 @@ import kotlin.time.Instant * - [`$[]`][all] * - [`$[]`][filter] * - [`$addToSet`][addToSet] + * - [`$pop`][popLast] (last) + * - [`$pop`][popFirst] (first) * - [`$push`][push] * * Time management: @@ -1496,6 +1498,127 @@ interface UpdateQuery : CompoundBsonNode, FieldDsl { return this.field.addEachToSet(values) } + // endregion + // region $pop + + /** + * Removes the last element in the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * fiilter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores.popLast() + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pop/) + */ + @KtMongoDsl + fun Field>.popLast() + + /** + * Removes the last element in the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * fiilter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores.popLast() + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pop/) + */ + @KtMongoDsl + fun kotlin.reflect.KProperty1>.popLast() { + return this.field.popLast() + } + + /** + * Removes the first element in the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * fiilter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores.popFirst() + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pop/) + */ + fun Field>.popFirst() + + /** + * Removes the first element in the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * val scores: List, + * ) + * + * collection.updateOne( + * fiilter = { + * User::name eq "Bob" + * }, + * update = { + * User::scores.popFirst() + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pop/) + */ + fun kotlin.reflect.KProperty1>.popFirst() { + return this.field.popFirst() + } + // endregion // region $push diff --git a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt index cca675af..d66e1e83 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -400,6 +400,37 @@ private class UpdateQueryImpl( } } + // endregion + // region $pop + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun Field>.popLast() { + accept(PopBsonNode(listOf(this.path to 1), context)) + } + + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + override fun Field>.popFirst() { + accept(PopBsonNode(listOf(this.path to -1), context)) + } + + @LowLevelApi + private class PopBsonNode( + val mappings: List>, + context: BsonContext, + ) : UpdateBsonNodeNode(context) { + + override fun simplify() = + this.takeUnless { mappings.isEmpty() } + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument($$"$pop") { + for ((field, direction) in mappings) { + writeInt32(field.toString(), direction) + } + } + } + } + // endregion // region $push @@ -657,6 +688,9 @@ private class UpdateQueryImpl( OperatorCombinator(AddToSetBsonNode::class) { sources, context -> AddToSetBsonNode(sources.flatMap { it.mappings }, context) }, + OperatorCombinator(PopBsonNode::class) { sources, context -> + PopBsonNode(sources.flatMap { it.mappings }, context) + }, OperatorCombinator(PushBsonNode::class) { sources, context -> PushBsonNode(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 cde2bce0..db9d053d 100644 --- a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt +++ b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt @@ -399,6 +399,48 @@ val FieldUpdateTest by multiContextSuite { } } + suite($$"$pop") { + test("Pop last") { + update { + User::tokens.popLast() + } shouldBeBson $$""" + { + "$pop": { + "tokens": 1 + } + } + """.trimIndent() + } + + test("Pop first") { + update { + User::tokens.popFirst() + } shouldBeBson $$""" + { + "$pop": { + "tokens": -1 + } + } + """.trimIndent() + } + + test("Pop multiple") { + update { + User::friends.popFirst() + User::tokens.popLast() + User::scores.popLast() + } shouldBeBson $$""" + { + "$pop": { + "friends": -1, + "tokens": 1, + "scores": 1 + } + } + """.trimIndent() + } + } + suite($$"$push") { test("Add a single field") { update { -- 2.51.2 From fe025cca367e16a2c2506b327d459871316671cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 29 May 2026 16:00:38 +0200 Subject: [PATCH 2/3] feat(dsl): Add $pull (update) --- .../commonMain/kotlin/query/UpdateQuery.kt | 189 ++++++++++++ .../kotlin/query/UpdateQueryImpl.kt | 58 ++++ .../commonMain/kotlin/query/UpdateQuery.kt | 285 ++++++++++++++++++ .../kotlin/query/UpdateQueryImpl.kt | 58 ++++ .../kotlin/query/update/FieldUpdateTest.kt | 71 +++++ test/src/commonTest/kotlin/ArraysTest.kt | 40 +++ 6 files changed, 701 insertions(+) diff --git a/dsl-template/src/commonMain/kotlin/query/UpdateQuery.kt b/dsl-template/src/commonMain/kotlin/query/UpdateQuery.kt index a32512c3..e423c040 100644 --- a/dsl-template/src/commonMain/kotlin/query/UpdateQuery.kt +++ b/dsl-template/src/commonMain/kotlin/query/UpdateQuery.kt @@ -70,6 +70,7 @@ import kotlin.time.Instant * - [`$addToSet`][addToSet] * - [`$pop`][popLast] (last) * - [`$pop`][popFirst] (first) + * - [`$pull`][pull] * - [`$push`][push] * * Time management: @@ -1621,7 +1622,195 @@ interface UpdateQuery : CompoundBsonNode, FieldDsl { ): Field // endregion + // region $pull + /** + * Removes all instances of [value] from the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull 10 + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field>.pull(value: V, type: KType) + + /** + * Removes all instances of [value] from the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull 10 + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE", "WRONG_MODIFIER_CONTAINING_DECLARATION") + @KtMongoDsl + final inline infix fun <@kotlin.internal.OnlyInputTypes reified V> Field>.pull(value: V) { + this.pull(value, typeOf()) + } + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on their own intrinsic value (e.g. whether the item is greater than some value), see [pullValues]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * class Grade( + * val name: String, + * val score: Int, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull { Gradle::score lte 10 } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field>.pull(predicate: FilterQuery.() -> Unit, type: KType) + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on their own intrinsic value (e.g. whether the item is greater than some value), see [pullValues]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * class Grade( + * val name: String, + * val score: Int, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull { Gradle::score lte 10 } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE", "WRONG_MODIFIER_CONTAINING_DECLARATION") + @KtMongoDsl + final inline infix fun <@kotlin.internal.OnlyInputTypes reified V> Field>.pull(noinline predicate: FilterQuery.() -> Unit) { + this.pull(predicate, typeOf()) + } + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on a value of one or multiple of their fields, see [pull]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pullValues { lte(10) } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field>.pullValues(predicate: FilterQueryPredicate.() -> Unit, type: KType) + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on a value of one or multiple of their fields, see [pull]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pullValues { lte(10) } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE", "WRONG_MODIFIER_CONTAINING_DECLARATION") + @KtMongoDsl + final inline infix fun <@kotlin.internal.OnlyInputTypes reified V> Field>.pullValues(noinline predicate: FilterQueryPredicate.() -> Unit) { + this.pullValues(predicate, typeOf()) + } + + // endregion } /** diff --git a/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt index a998e21e..4e6ae6ad 100644 --- a/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -653,6 +653,61 @@ private class UpdateQueryImpl( return filter(id) } + // endregion + // region $pull + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun Field>.pull(value: V, type: KType) { + accept(PullBsonNode(listOf(path to Value(value, type)), emptyList(), context)) + } + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun Field>.pull(predicate: FilterQuery.() -> Unit, type: KType) { + accept(PullBsonNode(emptyList(), listOf(path to FilterQuery(context).apply(predicate)), context)) + } + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun Field>.pullValues(predicate: FilterQueryPredicate.() -> Unit, type: KType) { + accept(PullBsonNode(emptyList(), listOf(path to FilterQueryPredicate(context, type).apply(predicate)), context)) + } + + @OptIn(LowLevelApi::class) + private class PullBsonNode( + val valueMappings: List>, + val predicateMappings: List>, + context: BsonContext, + ) : UpdateBsonNodeNode(context) { + + override fun simplify(): PullBsonNode? { + val simplifiedPredicateMappings = predicateMappings.mapNotNull { (path, value) -> + val simplified = value.simplify() ?: return@mapNotNull null + path to simplified + } + + if (valueMappings.isEmpty() && predicateMappings.isEmpty()) + return null + + if (predicateMappings == simplifiedPredicateMappings) + return this + + return PullBsonNode(valueMappings, simplifiedPredicateMappings, context) + } + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument($$"$pull") { + for ((field, value) in valueMappings) { + writeSafe(field.toString(), value.value, value.type) + } + + for ((field, predicate) in predicateMappings) { + writeDocument(field.toString()) { + predicate.writeTo(this) + } + } + } + } + } + // endregion companion object { @@ -688,6 +743,9 @@ private class UpdateQueryImpl( OperatorCombinator(PopBsonNode::class) { sources, context -> PopBsonNode(sources.flatMap { it.mappings }, context) }, + OperatorCombinator(PullBsonNode::class) { sources, context -> + PullBsonNode(sources.flatMap { it.valueMappings }, sources.flatMap { it.predicateMappings }, context) + }, OperatorCombinator(PushBsonNode::class) { sources, context -> PushBsonNode(sources.flatMap { it.mappings }, context) }, diff --git a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt index 82ca035b..5d5a31bd 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQuery.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQuery.kt @@ -73,6 +73,7 @@ import kotlin.time.Instant * - [`$addToSet`][addToSet] * - [`$pop`][popLast] (last) * - [`$pop`][popFirst] (first) + * - [`$pull`][pull] * - [`$push`][push] * * Time management: @@ -2538,7 +2539,291 @@ interface UpdateQuery : CompoundBsonNode, FieldDsl { } // endregion + // region $pull + /** + * Removes all instances of [value] from the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull 10 + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field>.pull(value: V, type: KType) + + /** + * Removes all instances of [value] from the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull 10 + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE", "WRONG_MODIFIER_CONTAINING_DECLARATION") + @KtMongoDsl + final inline infix fun <@kotlin.internal.OnlyInputTypes reified V> Field>.pull(value: V) { + this.pull(value, typeOf()) + } + + /** + * Removes all instances of [value] from the specified array. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull 10 + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE", "WRONG_MODIFIER_CONTAINING_DECLARATION") + @KtMongoDsl + final inline infix fun <@kotlin.internal.OnlyInputTypes reified V> kotlin.reflect.KProperty1>.pull(value: V) { + return this.field.pull(value) + } + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on their own intrinsic value (e.g. whether the item is greater than some value), see [pullValues]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * class Grade( + * val name: String, + * val score: Int, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull { Gradle::score lte 10 } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field>.pull(predicate: FilterQuery.() -> Unit, type: KType) + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on their own intrinsic value (e.g. whether the item is greater than some value), see [pullValues]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * class Grade( + * val name: String, + * val score: Int, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull { Gradle::score lte 10 } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE", "WRONG_MODIFIER_CONTAINING_DECLARATION") + @KtMongoDsl + final inline infix fun <@kotlin.internal.OnlyInputTypes reified V> Field>.pull(noinline predicate: FilterQuery.() -> Unit) { + this.pull(predicate, typeOf()) + } + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on their own intrinsic value (e.g. whether the item is greater than some value), see [pullValues]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * class Grade( + * val name: String, + * val score: Int, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pull { Gradle::score lte 10 } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE", "WRONG_MODIFIER_CONTAINING_DECLARATION") + @KtMongoDsl + final inline infix fun <@kotlin.internal.OnlyInputTypes reified V> kotlin.reflect.KProperty1>.pull(noinline predicate: FilterQuery.() -> Unit) { + return this.field.pull(predicate) + } + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on a value of one or multiple of their fields, see [pull]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pullValues { lte(10) } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE") + @KtMongoDsl + fun <@kotlin.internal.OnlyInputTypes V> Field>.pullValues(predicate: FilterQueryPredicate.() -> Unit, type: KType) + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on a value of one or multiple of their fields, see [pull]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pullValues { lte(10) } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE", "WRONG_MODIFIER_CONTAINING_DECLARATION") + @KtMongoDsl + final inline infix fun <@kotlin.internal.OnlyInputTypes reified V> Field>.pullValues(noinline predicate: FilterQueryPredicate.() -> Unit) { + this.pullValues(predicate, typeOf()) + } + + /** + * Removes all items of an array that match [predicate]. + * + * To select items based on a value of one or multiple of their fields, see [pull]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val tests: List, + * ) + * + * users.updateOne( + * filter = { User::name eq "Paul" }, + * update = { + * User::tests pullValues { lte(10) } + * } + * ) + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/operator/update/pull/) + */ + @Suppress("INVISIBLE_REFERENCE", "WRONG_MODIFIER_CONTAINING_DECLARATION") + @KtMongoDsl + final inline infix fun <@kotlin.internal.OnlyInputTypes reified V> kotlin.reflect.KProperty1>.pullValues(noinline predicate: FilterQueryPredicate.() -> Unit) { + return this.field.pullValues(predicate) + } + + // endregion } /** diff --git a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt index d66e1e83..26785b53 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -656,6 +656,61 @@ private class UpdateQueryImpl( return filter(id) } + // endregion + // region $pull + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun Field>.pull(value: V, type: KType) { + accept(PullBsonNode(listOf(path to Value(value, type)), emptyList(), context)) + } + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun Field>.pull(predicate: FilterQuery.() -> Unit, type: KType) { + accept(PullBsonNode(emptyList(), listOf(path to FilterQuery(context).apply(predicate)), context)) + } + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + override fun Field>.pullValues(predicate: FilterQueryPredicate.() -> Unit, type: KType) { + accept(PullBsonNode(emptyList(), listOf(path to FilterQueryPredicate(context, type).apply(predicate)), context)) + } + + @OptIn(LowLevelApi::class) + private class PullBsonNode( + val valueMappings: List>, + val predicateMappings: List>, + context: BsonContext, + ) : UpdateBsonNodeNode(context) { + + override fun simplify(): PullBsonNode? { + val simplifiedPredicateMappings = predicateMappings.mapNotNull { (path, value) -> + val simplified = value.simplify() ?: return@mapNotNull null + path to simplified + } + + if (valueMappings.isEmpty() && predicateMappings.isEmpty()) + return null + + if (predicateMappings == simplifiedPredicateMappings) + return this + + return PullBsonNode(valueMappings, simplifiedPredicateMappings, context) + } + + override fun write(writer: BsonFieldWriter) = with(writer) { + writeDocument($$"$pull") { + for ((field, value) in valueMappings) { + writeSafe(field.toString(), value.value, value.type) + } + + for ((field, predicate) in predicateMappings) { + writeDocument(field.toString()) { + predicate.writeTo(this) + } + } + } + } + } + // endregion companion object { @@ -691,6 +746,9 @@ private class UpdateQueryImpl( OperatorCombinator(PopBsonNode::class) { sources, context -> PopBsonNode(sources.flatMap { it.mappings }, context) }, + OperatorCombinator(PullBsonNode::class) { sources, context -> + PullBsonNode(sources.flatMap { it.valueMappings }, sources.flatMap { it.predicateMappings }, context) + }, OperatorCombinator(PushBsonNode::class) { sources, context -> PushBsonNode(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 db9d053d..40fb9b45 100644 --- a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt +++ b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt @@ -441,6 +441,77 @@ val FieldUpdateTest by multiContextSuite { } } + suite($$"$pull") { + test("Pull a single value") { + update { + User::tokens pull "first" + } shouldBeBson $$""" + { + "$pull": { + "tokens": "first" + } + } + """.trimIndent() + } + + test("Pull multiple different values") { + update { + User::tokens pull "first" + User::scores pull 3 + } shouldBeBson $$""" + { + "$pull": { + "tokens": "first", + "scores": 3 + } + } + """.trimIndent() + } + + test("Pull elements using a condition") { + update { + User::friends pull { + Friend::name eq "Alice" + } + } shouldBeBson $$""" + { + "$pull": { + "friends": { + "name": { + "$eq": "Alice" + } + } + } + } + """.trimIndent() + } + + test("Pull elements using a value condition") { + update { + User::scores pullValues { + gte(5) + lt(10) + } + + User::tokens pullValues { + isOneOf("first", "second", "third", "fourth") + } + } shouldBeBson $$""" + { + "$pull": { + "scores": { + "$gte": 5, + "$lt": 10 + }, + "tokens": { + "$in": ["first", "second", "third", "fourth"] + } + } + } + """.trimIndent() + } + } + suite($$"$push") { test("Add a single field") { update { diff --git a/test/src/commonTest/kotlin/ArraysTest.kt b/test/src/commonTest/kotlin/ArraysTest.kt index a4e5f399..79b5414b 100644 --- a/test/src/commonTest/kotlin/ArraysTest.kt +++ b/test/src/commonTest/kotlin/ArraysTest.kt @@ -135,6 +135,46 @@ val ArraysTest by preparedSuite(preparedConfig = CoroutineTimeout(30.seconds)) { check(expected == profiles.find().toList()) } + test($$"pull") { + users().insertOne( + ArrayUser( + name = "Bob", + grades = listOf(17, 8, 9, 3), + friends = listOf( + ArrayUser("Fred", grades = listOf(1, 7)), + ArrayUser("Alice", grades = listOf(18, 16)), + ArrayUser("Julia", grades = listOf(13)), + ) + ) + ) + + users().updateOne { + ArrayUser::grades pullValues { + gt(4) + lte(10) + } + } + + check(users().findOne {}?.grades == listOf(17, 3)) + + users().updateOne { + ArrayUser::grades pull 3 + } + + check(users().findOne {}?.grades == listOf(17)) + + users().updateOne { + ArrayUser::friends pull { + or { + ArrayUser::name gte "I" // Removes Julia + ArrayUser::grades.any lt 10 // Removes Fred + } + } + } + + check(users().findOne {}?.friends == listOf(ArrayUser("Alice", grades = listOf(18, 16)))) + } + suite("Array filters") { test("On the value itself") { users().insertOne( -- 2.51.2 From 65ad26c021bcccd874db97692560f64f73fdb945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 29 May 2026 16:36:14 +0200 Subject: [PATCH 3/3] feat(dsl): Combine multiple simple $pull using a $in --- .../kotlin/query/UpdateQueryImpl.kt | 30 +++++++++++++++++-- .../kotlin/query/UpdateQueryImpl.kt | 30 +++++++++++++++++-- .../kotlin/query/update/FieldUpdateTest.kt | 15 ++++++++++ 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt index 4e6ae6ad..f123b6f1 100644 --- a/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl-template/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -679,18 +679,42 @@ private class UpdateQueryImpl( ) : UpdateBsonNodeNode(context) { override fun simplify(): PullBsonNode? { - val simplifiedPredicateMappings = predicateMappings.mapNotNull { (path, value) -> + // ①. If a key appears multiple times in 'valueMappings', combine it with $in and move it into 'predicateMappings' + + val duplicatedValueMappings = valueMappings + .groupingBy { it.first } + .fold(0 to emptyList()) { (count, mappings), newMappings -> + (count + 1) to (mappings + newMappings.second) + } + .filterValues { (count, _) -> count > 1 } + + val valueMappingsWithDuplicated = valueMappings.filter { (path, _) -> + path !in duplicatedValueMappings + } + + val predicateMappingsWithDuplicated = predicateMappings + duplicatedValueMappings + .map { (path, data) -> + val type = data.second.mapTo(HashSet()) { it.type }.singleOrNull() + ?: error("Cannot call the pull operator multiple times with values of different types. Please explicitly use the 'pull { isOneOf(…) }' syntax. Values: ${data.second}") + path to FilterQueryPredicate(context, type).apply { + isOneOf(data.second.map { it.value }) + } + } + + // ②. Simplify 'predicateMappings' + + val simplifiedPredicateMappings = predicateMappingsWithDuplicated.mapNotNull { (path, value) -> val simplified = value.simplify() ?: return@mapNotNull null path to simplified } - if (valueMappings.isEmpty() && predicateMappings.isEmpty()) + if (valueMappingsWithDuplicated.isEmpty() && predicateMappingsWithDuplicated.isEmpty()) return null if (predicateMappings == simplifiedPredicateMappings) return this - return PullBsonNode(valueMappings, simplifiedPredicateMappings, context) + return PullBsonNode(valueMappingsWithDuplicated, simplifiedPredicateMappings, context) } override fun write(writer: BsonFieldWriter) = with(writer) { diff --git a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt index 26785b53..9d2bdc55 100644 --- a/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt +++ b/dsl/src/commonMain/kotlin/query/UpdateQueryImpl.kt @@ -682,18 +682,42 @@ private class UpdateQueryImpl( ) : UpdateBsonNodeNode(context) { override fun simplify(): PullBsonNode? { - val simplifiedPredicateMappings = predicateMappings.mapNotNull { (path, value) -> + // ①. If a key appears multiple times in 'valueMappings', combine it with $in and move it into 'predicateMappings' + + val duplicatedValueMappings = valueMappings + .groupingBy { it.first } + .fold(0 to emptyList()) { (count, mappings), newMappings -> + (count + 1) to (mappings + newMappings.second) + } + .filterValues { (count, _) -> count > 1 } + + val valueMappingsWithDuplicated = valueMappings.filter { (path, _) -> + path !in duplicatedValueMappings + } + + val predicateMappingsWithDuplicated = predicateMappings + duplicatedValueMappings + .map { (path, data) -> + val type = data.second.mapTo(HashSet()) { it.type }.singleOrNull() + ?: error("Cannot call the pull operator multiple times with values of different types. Please explicitly use the 'pull { isOneOf(…) }' syntax. Values: ${data.second}") + path to FilterQueryPredicate(context, type).apply { + isOneOf(data.second.map { it.value }) + } + } + + // ②. Simplify 'predicateMappings' + + val simplifiedPredicateMappings = predicateMappingsWithDuplicated.mapNotNull { (path, value) -> val simplified = value.simplify() ?: return@mapNotNull null path to simplified } - if (valueMappings.isEmpty() && predicateMappings.isEmpty()) + if (valueMappingsWithDuplicated.isEmpty() && predicateMappingsWithDuplicated.isEmpty()) return null if (predicateMappings == simplifiedPredicateMappings) return this - return PullBsonNode(valueMappings, simplifiedPredicateMappings, context) + return PullBsonNode(valueMappingsWithDuplicated, simplifiedPredicateMappings, context) } override fun write(writer: BsonFieldWriter) = with(writer) { diff --git a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt index 40fb9b45..b81e761d 100644 --- a/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt +++ b/dsl/src/commonTest/kotlin/query/update/FieldUpdateTest.kt @@ -468,6 +468,21 @@ val FieldUpdateTest by multiContextSuite { """.trimIndent() } + test("Pull from the same array multiple times") { + update { + User::tokens pull "first" + User::tokens pull "second" + } shouldBeBson $$""" + { + "$pull": { + "tokens": { + "$in": ["first", "second"] + } + } + } + """.trimIndent() + } + test("Pull elements using a condition") { update { User::friends pull { -- 2.51.2