diff --git a/dsl/src/commonMain/kotlin/expr/UpdateExpression.kt b/dsl/src/commonMain/kotlin/expr/UpdateExpression.kt index d8d69424..acbf11ad 100644 --- a/dsl/src/commonMain/kotlin/expr/UpdateExpression.kt +++ b/dsl/src/commonMain/kotlin/expr/UpdateExpression.kt @@ -24,10 +24,10 @@ import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.expr.common.AbstractCompoundExpression import opensavvy.ktmongo.dsl.expr.common.AbstractExpression import opensavvy.ktmongo.dsl.expr.common.Expression -import opensavvy.ktmongo.dsl.expr.common.acceptAll import opensavvy.ktmongo.dsl.path.Field import opensavvy.ktmongo.dsl.path.FieldDsl import opensavvy.ktmongo.dsl.path.Path +import opensavvy.ktmongo.dsl.tree.acceptAll import kotlin.reflect.KClass /** diff --git a/dsl/src/commonMain/kotlin/expr/common/CompoundExpression.kt b/dsl/src/commonMain/kotlin/expr/common/CompoundExpression.kt index d0b0fd06..0eb2dbc5 100644 --- a/dsl/src/commonMain/kotlin/expr/common/CompoundExpression.kt +++ b/dsl/src/commonMain/kotlin/expr/common/CompoundExpression.kt @@ -132,17 +132,3 @@ abstract class AbstractCompoundExpression( companion object } - -/** - * Adds any number of [expressions] into this one. - * - * To learn more about the behavior of this function and the security implications, see [accept][CompoundExpression.accept]. - */ -@LowLevelApi -@DangerousMongoApi -@KtMongoDsl -fun CompoundExpression.acceptAll(expressions: Iterable) { - for (child in expressions) { - accept(child) - } -} diff --git a/dsl/src/commonMain/kotlin/tree/CompoundNode.kt b/dsl/src/commonMain/kotlin/tree/CompoundNode.kt index f8121819..ddf18993 100644 --- a/dsl/src/commonMain/kotlin/tree/CompoundNode.kt +++ b/dsl/src/commonMain/kotlin/tree/CompoundNode.kt @@ -58,3 +58,17 @@ interface CompoundNode { fun accept(node: N) } + +/** + * Adds any number of [nodes] into this one. + * + * To learn more about the behavior of this function and the security implications, see [accept][CompoundNode.accept]. + */ +@LowLevelApi +@DangerousMongoApi +@KtMongoDsl +fun CompoundNode.acceptAll(nodes: Iterable) { + for (child in nodes) { + accept(child) + } +} -- 2.51.2 From 430252b95815618e4b75b32864becae537fc4a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Nov 2024 22:24:07 +0100 Subject: [PATCH 2/8] refactor(dsl): Move Expression.toBsonDocument into :dsl --- .../src/jvmMain/kotlin/JvmMongoCollection.kt | 10 +--------- driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt | 10 ++-------- dsl/src/commonMain/kotlin/expr/common/Expression.kt | 10 ++++++++++ 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt index eda40a29..481c2448 100644 --- a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -19,17 +19,15 @@ package opensavvy.ktmongo.coroutines import com.mongodb.client.model.FindOneAndUpdateOptions import com.mongodb.client.model.UpdateOptions import opensavvy.ktmongo.bson.BsonContext -import opensavvy.ktmongo.bson.buildBsonDocument import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.expr.* -import opensavvy.ktmongo.dsl.expr.common.Expression +import opensavvy.ktmongo.dsl.expr.common.toBsonDocument import opensavvy.ktmongo.dsl.models.* import opensavvy.ktmongo.dsl.options.CountOptions import opensavvy.ktmongo.dsl.options.FindOptions import opensavvy.ktmongo.dsl.options.common.LimitOption import opensavvy.ktmongo.dsl.options.common.option import opensavvy.ktmongo.dsl.options.toJava -import org.bson.BsonDocument /** * Implementation of [MongoCollection] based on [MongoDB's MongoCollection][com.mongodb.kotlin.client.coroutine.MongoCollection]. @@ -162,12 +160,6 @@ class JvmMongoCollection internal constructor( } -@OptIn(LowLevelApi::class) -private fun Expression.toBsonDocument(): BsonDocument = - buildBsonDocument { - writeTo(this) - } - /** * Converts a [MongoDB collection][com.mongodb.kotlin.client.coroutine.MongoCollection] into a [KtMongo collection][JvmMongoCollection]. */ diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt index 119cbdd6..081bc832 100644 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -19,17 +19,15 @@ package opensavvy.ktmongo.sync import com.mongodb.client.model.FindOneAndUpdateOptions import com.mongodb.client.model.UpdateOptions import opensavvy.ktmongo.bson.BsonContext -import opensavvy.ktmongo.bson.buildBsonDocument import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.expr.* -import opensavvy.ktmongo.dsl.expr.common.Expression +import opensavvy.ktmongo.dsl.expr.common.toBsonDocument import opensavvy.ktmongo.dsl.models.* import opensavvy.ktmongo.dsl.options.CountOptions import opensavvy.ktmongo.dsl.options.FindOptions import opensavvy.ktmongo.dsl.options.common.LimitOption import opensavvy.ktmongo.dsl.options.common.option import opensavvy.ktmongo.dsl.options.toJava -import org.bson.BsonDocument /** * Implementation of [MongoCollection] based on [MongoDB's MongoCollection][com.mongodb.kotlin.client.MongoCollection]. @@ -162,11 +160,7 @@ class JvmMongoCollection internal constructor( } -@OptIn(LowLevelApi::class) -private fun Expression.toBsonDocument(): BsonDocument = - buildBsonDocument { - writeTo(this) - } +} /** * Converts a [MongoDB collection][com.mongodb.kotlin.client.MongoCollection] into a [KtMongo collection][JvmMongoCollection]. diff --git a/dsl/src/commonMain/kotlin/expr/common/Expression.kt b/dsl/src/commonMain/kotlin/expr/common/Expression.kt index 21616d35..83737701 100644 --- a/dsl/src/commonMain/kotlin/expr/common/Expression.kt +++ b/dsl/src/commonMain/kotlin/expr/common/Expression.kt @@ -16,6 +16,7 @@ package opensavvy.ktmongo.dsl.expr.common +import opensavvy.ktmongo.bson.Bson import opensavvy.ktmongo.bson.BsonContext import opensavvy.ktmongo.bson.BsonFieldWriter import opensavvy.ktmongo.bson.buildBsonDocument @@ -81,6 +82,15 @@ interface Expression : Node { companion object } +/** + * Creates a new [BSON document][buildBsonDocument] containing the data from this expression. + */ +@LowLevelApi +fun Expression.toBsonDocument(): Bson = + buildBsonDocument { + writeTo(this) + } + /** * Utility implementation for [Expression], which handles the [context], [toString] representation and [freezing][freeze]. * -- 2.51.2 From a631c2778fb9b4844a280fcb01e247874b57863b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Nov 2024 22:26:41 +0100 Subject: [PATCH 3/8] test(driver-sync): Run each test in a specific collection --- .../src/commonTest/kotlin/TestDatabase.kt | 25 ++++++++++++++++++- .../src/jsTest/kotlin/TestDatabase.js.kt | 2 +- .../src/jvmTest/kotlin/TestDatabase.jvm.kt | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/driver-sync/src/commonTest/kotlin/TestDatabase.kt b/driver-sync/src/commonTest/kotlin/TestDatabase.kt index d868fcc0..2e58bd97 100644 --- a/driver-sync/src/commonTest/kotlin/TestDatabase.kt +++ b/driver-sync/src/commonTest/kotlin/TestDatabase.kt @@ -17,5 +17,28 @@ package opensavvy.ktmongo.sync import opensavvy.prepared.suite.PreparedProvider +import opensavvy.prepared.suite.cleanUp +import opensavvy.prepared.suite.prepared +import opensavvy.prepared.suite.randomInt -expect inline fun testCollection(name: String): PreparedProvider> +expect inline fun testCollectionExact(name: String): PreparedProvider> + +val collectionPostfix by randomInt(0, Int.MAX_VALUE) +inline fun testCollection(name: String): PreparedProvider> = prepared { + val name = "$name-${collectionPostfix()}" + + val realCollection by testCollectionExact(name) + + cleanUp("Log the collection after failed test", onSuccess = false) { + println("Collection $name with ${realCollection().count()} documents:") + realCollection().find().forEach { document -> + println(" • $document") + } + } + + cleanUp("Drop the collection", onFailure = false) { + println("Dropping the collection is not implemented yet") + } + + realCollection() +} diff --git a/driver-sync/src/jsTest/kotlin/TestDatabase.js.kt b/driver-sync/src/jsTest/kotlin/TestDatabase.js.kt index 97e2b8ab..2c56c181 100644 --- a/driver-sync/src/jsTest/kotlin/TestDatabase.js.kt +++ b/driver-sync/src/jsTest/kotlin/TestDatabase.js.kt @@ -18,6 +18,6 @@ package opensavvy.ktmongo.sync import opensavvy.prepared.suite.PreparedProvider -actual inline fun testCollection(name: String): PreparedProvider> { +actual inline fun testCollectionExact(name: String): PreparedProvider> { TODO("Not yet implemented") } diff --git a/driver-sync/src/jvmTest/kotlin/TestDatabase.jvm.kt b/driver-sync/src/jvmTest/kotlin/TestDatabase.jvm.kt index e696cabe..ebebe86d 100644 --- a/driver-sync/src/jvmTest/kotlin/TestDatabase.jvm.kt +++ b/driver-sync/src/jvmTest/kotlin/TestDatabase.jvm.kt @@ -35,7 +35,7 @@ internal val database by shared(CoroutineName("mongodb-establish-connection")) { client.getDatabase("ktmongo-sync-tests") } -actual inline fun testCollection(name: String): PreparedProvider> = prepared(CoroutineName("mongodb-create-collection-$name")) { +actual inline fun testCollectionExact(name: String): PreparedProvider> = prepared(CoroutineName("mongodb-create-collection-$name")) { val collection = database().getCollection(name) collection.asKtMongo() } -- 2.51.2 From 6f07e0a5cc76eb23c5cfebe9edb3b7b41ca04539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Nov 2024 22:28:58 +0100 Subject: [PATCH 4/8] feat: Improve MongoCollection.toString and FilteredCollection.toString --- .../src/commonMain/kotlin/FilteredCollection.kt | 11 +++++++++++ .../src/jvmMain/kotlin/JvmMongoCollection.kt | 3 +++ .../src/commonMain/kotlin/FilteredCollection.kt | 9 +++++++++ driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt | 3 ++- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/driver-coroutines/src/commonMain/kotlin/FilteredCollection.kt b/driver-coroutines/src/commonMain/kotlin/FilteredCollection.kt index 54554bd3..5cfa21ae 100644 --- a/driver-coroutines/src/commonMain/kotlin/FilteredCollection.kt +++ b/driver-coroutines/src/commonMain/kotlin/FilteredCollection.kt @@ -18,9 +18,11 @@ package opensavvy.ktmongo.coroutines import opensavvy.ktmongo.bson.BsonContext import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.expr.FilterExpression import opensavvy.ktmongo.dsl.expr.FilterOperators import opensavvy.ktmongo.dsl.expr.UpdateOperators import opensavvy.ktmongo.dsl.expr.UpsertOperators +import opensavvy.ktmongo.dsl.expr.common.toBsonDocument import opensavvy.ktmongo.dsl.options.CountOptions import opensavvy.ktmongo.dsl.options.FindOptions import opensavvy.ktmongo.dsl.options.UpdateOptions @@ -122,6 +124,15 @@ private class FilteredCollection( }, update = update, ) + + @OptIn(LowLevelApi::class) + override fun toString(): String { + val filter = FilterExpression(context) + .apply(globalFilter) + .toBsonDocument() + + return "$upstream.filter $filter" + } } /** diff --git a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt index 481c2448..b2d12354 100644 --- a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -158,6 +158,9 @@ class JvmMongoCollection internal constructor( // endregion + override fun toString(): String = + "MongoCollection(${inner.namespace})" + } /** diff --git a/driver-sync/src/commonMain/kotlin/FilteredCollection.kt b/driver-sync/src/commonMain/kotlin/FilteredCollection.kt index 4fced667..32cec8f6 100644 --- a/driver-sync/src/commonMain/kotlin/FilteredCollection.kt +++ b/driver-sync/src/commonMain/kotlin/FilteredCollection.kt @@ -122,6 +122,15 @@ private class FilteredCollection( }, update = update, ) + + @OptIn(LowLevelApi::class) + override fun toString(): String { + val filter = FilterExpression(context) + .apply(globalFilter) + .toBsonDocument() + + return "$upstream.filter $filter" + } } /** diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt index 081bc832..361debe9 100644 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -158,7 +158,8 @@ class JvmMongoCollection internal constructor( // endregion -} + override fun toString(): String = + "MongoCollection(${inner.namespace})" } -- 2.51.2 From 305b818d2e9fff3d8e6f59afc91d7b4f7f446813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Nov 2024 22:31:14 +0100 Subject: [PATCH 5/8] refactor(dsl): Create helper to implement immutable nodes --- dsl/src/commonMain/kotlin/tree/Node.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dsl/src/commonMain/kotlin/tree/Node.kt b/dsl/src/commonMain/kotlin/tree/Node.kt index 1ca5f32c..4a5aab84 100644 --- a/dsl/src/commonMain/kotlin/tree/Node.kt +++ b/dsl/src/commonMain/kotlin/tree/Node.kt @@ -17,6 +17,7 @@ package opensavvy.ktmongo.dsl.tree import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.tree.ImmutableNode.freeze /** * An element in an abstract tree. @@ -75,3 +76,13 @@ internal class NodeImpl : Node { frozen = true } } + +/** + * Helper to represent a [Node] that can never mutate, even if it hasn't been [frozen][freeze] yet. + * + * Should generally be used to implement [Node] by delegation. + */ +internal object ImmutableNode : Node { + @LowLevelApi + override fun freeze() {} +} -- 2.51.2 From b48163d91975882c63b081c625ac49da5a41b971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Nov 2024 22:33:19 +0100 Subject: [PATCH 6/8] feat(dsl): Create the BulkWrite model --- dsl/src/commonMain/kotlin/models/BulkWrite.kt | 277 ++++++++++++++++++ dsl/src/commonMain/kotlin/models/Update.kt | 8 +- .../kotlin/options/BulkWriteOptions.kt | 27 ++ .../jvmMain/kotlin/models/BulkWrite.jvm.kt | 45 +++ 4 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 dsl/src/commonMain/kotlin/models/BulkWrite.kt create mode 100644 dsl/src/commonMain/kotlin/options/BulkWriteOptions.kt create mode 100644 dsl/src/jvmMain/kotlin/models/BulkWrite.jvm.kt diff --git a/dsl/src/commonMain/kotlin/models/BulkWrite.kt b/dsl/src/commonMain/kotlin/models/BulkWrite.kt new file mode 100644 index 00000000..2d666cb6 --- /dev/null +++ b/dsl/src/commonMain/kotlin/models/BulkWrite.kt @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2024, 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.models + +import opensavvy.ktmongo.bson.BsonContext +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.expr.FilterOperators +import opensavvy.ktmongo.dsl.expr.UpdateOperators +import opensavvy.ktmongo.dsl.expr.UpsertOperators +import opensavvy.ktmongo.dsl.options.BulkWriteOptions +import opensavvy.ktmongo.dsl.options.UpdateOptions +import opensavvy.ktmongo.dsl.tree.CompoundNode +import opensavvy.ktmongo.dsl.tree.Node +import opensavvy.ktmongo.dsl.tree.acceptAll + +sealed interface AvailableInBulkWrite : Node + +/** + * Performing multiple write operations in a single request. + * + * ### Example + * + * ```kotlin + * users.bulkWrite { + * updateOne({ User::name eq "foo" }) { + * User::age set 18 + * } + * + * upsertOne({ User::name eq "bob" }) { + * User::age setOnInsert 18 + * User::age inc 1 + * } + * } + * ``` + * + * ### Filtered writes + * + * If we have multiple writes that share a similar filter, we can extract it to be common between them. + * + * ```kotlin + * users.bulkWrite { + * updateOne({ User::name eq "foo" }) { + * User::age set 18 + * } + * + * filtered({ User::isAlive eq true }) { + * updateMany({ User::name eq "bar" }) { + * User::age inc 2 + * } + * + * updateOne({ User::name eq "baz" }) { + * User::age inc 1 + * } + * } + * } + * ``` + * + * To learn more, see [filtered]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/) + * + * @see BulkWriteOptions Options + */ +@KtMongoDsl +class BulkWrite private constructor( + val context: BsonContext, + private val globalFilter: FilterOperators.() -> Unit, + val options: BulkWriteOptions, +) : CompoundNode> { + + private val _operations = ArrayList>() + val operations: Sequence> get() = _operations.asSequence() + + constructor(context: BsonContext, globalFilter: FilterOperators.() -> Unit) : this(context, globalFilter, BulkWriteOptions(context)) + + @LowLevelApi + @DangerousMongoApi + override fun accept(node: AvailableInBulkWrite) { + _operations += node + } + + /** + * Declares a [filter] that is shared between all children [operations]. + * + * ### Example + * + * Sometimes, we have multiple operations in a single bulk write that share the same filter. + * This method allows to declare it a single time. + * + * ```kotlin + * users.bulkWrite { + * filtered({ User::isAlive eq true }) { + * updateOne { /* … */ } + * updateOne { /* … */ } + * } + * } + * ``` + */ + fun filtered( + filter: FilterOperators.() -> Unit, + operations: BulkWrite.() -> Unit, + ) { + val parent = this + + val child = BulkWrite( + context = context, + globalFilter = { + parent.globalFilter(this) + filter() + } + ) + + child.operations() + + @OptIn(LowLevelApi::class, DangerousMongoApi::class) + acceptAll(child.operations.asIterable()) + } + + /** + * Updates all documents that match [filter] according to [update]. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * updateMany( + * filter = { User::name eq "Patrick" }, + * update = { + * User::age set 15 + * } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#updateone-and-updatemany) + * + * @see updateOne Update a single document. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun updateMany( + options: UpdateOptions.() -> Unit = {}, + filter: FilterOperators.() -> Unit = {}, + update: UpdateOperators.() -> Unit, + ) { + val model = UpdateMany(context) + + model.options.options() + model.filter.globalFilter() + model.filter.filter() + model.update.update() + + accept(model) + } + + /** + * Updates a single document that matches [filter] according to [update]. + * + * If multiple documents match [filter], only the first one found is updated. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * updateOne( + * filter = { User::name eq "Patrick" }, + * update = { + * User::age set 15 + * } + * ) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#updateone-and-updatemany) + * + * @see updateMany Update multiple documents. + * @see upsertOne Create a document if none are found. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun updateOne( + options: UpdateOptions.() -> Unit = {}, + filter: FilterOperators.() -> Unit = {}, + update: UpdateOperators.() -> Unit, + ) { + val model = UpdateOne(context) + + model.options.options() + model.filter.globalFilter() + model.filter.filter() + model.update.update() + + accept(model) + } + + /** + * Updates a single document that matches [filter] according to [update]. + * + * If multiple documents match [filter], only the first one found is updated. + * + * If no documents match [filter], a new one is created. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * upsertOne( + * filter = { User::name eq "Patrick" }, + * update = { + * User::age set 15 + * } + * ) + * } + * ``` + * + * If a document exists that has the `name` of "Patrick", its age is set to 15. + * If none exist, a document with `name` "Patrick" and `age` 15 is created. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite/#updateone-and-updatemany) + * - [The behavior of upsert functions](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/#insert-a-new-document-if-no-match-exists--upsert-) + * + * @see updateOne Do nothing if no matching documents are found. + */ + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun upsertOne( + options: UpdateOptions.() -> Unit = {}, + filter: FilterOperators.() -> Unit = {}, + update: UpsertOperators.() -> Unit, + ) { + val model = UpsertOne(context) + + model.options.options() + model.filter.globalFilter() + model.filter.filter() + model.update.update() + + accept(model) + } + +} diff --git a/dsl/src/commonMain/kotlin/models/Update.kt b/dsl/src/commonMain/kotlin/models/Update.kt index 34a0d855..044668a5 100644 --- a/dsl/src/commonMain/kotlin/models/Update.kt +++ b/dsl/src/commonMain/kotlin/models/Update.kt @@ -20,6 +20,8 @@ import opensavvy.ktmongo.bson.BsonContext import opensavvy.ktmongo.dsl.KtMongoDsl import opensavvy.ktmongo.dsl.expr.* import opensavvy.ktmongo.dsl.options.UpdateOptions +import opensavvy.ktmongo.dsl.tree.ImmutableNode +import opensavvy.ktmongo.dsl.tree.Node /** * Updating a single element in a collection. @@ -41,7 +43,7 @@ class UpdateOne private constructor( val options: UpdateOptions, val filter: FilterOperators, val update: UpdateOperators, -) { +) : AvailableInBulkWrite, Node by ImmutableNode { constructor(context: BsonContext) : this(context, UpdateOptions(context), FilterExpression(context), UpdateExpression(context)) } @@ -66,7 +68,7 @@ class UpsertOne private constructor( val options: UpdateOptions, val filter: FilterOperators, val update: UpsertOperators, -) { +) : AvailableInBulkWrite, Node by ImmutableNode { constructor(context: BsonContext) : this(context, UpdateOptions(context), FilterExpression(context), UpdateExpression(context)) } @@ -91,7 +93,7 @@ class UpdateMany private constructor( val options: UpdateOptions, val filter: FilterOperators, val update: UpdateOperators, -) { +) : AvailableInBulkWrite, Node by ImmutableNode { constructor(context: BsonContext) : this(context, UpdateOptions(context), FilterExpression(context), UpdateExpression(context)) } diff --git a/dsl/src/commonMain/kotlin/options/BulkWriteOptions.kt b/dsl/src/commonMain/kotlin/options/BulkWriteOptions.kt new file mode 100644 index 00000000..dd07d99a --- /dev/null +++ b/dsl/src/commonMain/kotlin/options/BulkWriteOptions.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024, 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.options + +import opensavvy.ktmongo.bson.BsonContext +import opensavvy.ktmongo.dsl.options.common.Options +import opensavvy.ktmongo.dsl.options.common.OptionsHolder + +/** + * The options for a `collection.bulkWrite` operation. + */ +class BulkWriteOptions(context: BsonContext) : + Options by OptionsHolder(context) diff --git a/dsl/src/jvmMain/kotlin/models/BulkWrite.jvm.kt b/dsl/src/jvmMain/kotlin/models/BulkWrite.jvm.kt new file mode 100644 index 00000000..2832187b --- /dev/null +++ b/dsl/src/jvmMain/kotlin/models/BulkWrite.jvm.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, 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.models + +import com.mongodb.client.model.UpdateManyModel +import com.mongodb.client.model.UpdateOneModel +import com.mongodb.client.model.UpdateOptions +import com.mongodb.client.model.WriteModel +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.expr.common.toBsonDocument + +@OptIn(LowLevelApi::class) +fun AvailableInBulkWrite.toJava(): WriteModel = when (this) { + is UpdateMany<*> -> UpdateManyModel( + /* filter = */ this.filter.toBsonDocument(), + /* update = */ this.update.toBsonDocument(), + /* options = */ UpdateOptions(), + ) + + is UpdateOne<*> -> UpdateOneModel( + /* filter = */ this.filter.toBsonDocument(), + /* update = */ this.update.toBsonDocument(), + /* options = */ UpdateOptions(), + ) + + is UpsertOne<*> -> UpdateOneModel( + /* filter = */ this.filter.toBsonDocument(), + /* update = */ this.update.toBsonDocument(), + /* options = */ UpdateOptions().upsert(true), + ) +} -- 2.51.2 From c6c60d3973060557322091195789fa568b32066e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Nov 2024 22:36:36 +0100 Subject: [PATCH 7/8] feat(driver-sync): Introduce bulkWrite --- .../commonMain/kotlin/FilteredCollection.kt | 17 +++++ .../src/commonMain/kotlin/MongoCollection.kt | 1 + .../kotlin/operations/UpdateOperations.kt | 72 +++++++++++++++++++ .../src/jvmMain/kotlin/JvmMongoCollection.kt | 18 +++++ 4 files changed, 108 insertions(+) diff --git a/driver-sync/src/commonMain/kotlin/FilteredCollection.kt b/driver-sync/src/commonMain/kotlin/FilteredCollection.kt index 32cec8f6..c5e10a01 100644 --- a/driver-sync/src/commonMain/kotlin/FilteredCollection.kt +++ b/driver-sync/src/commonMain/kotlin/FilteredCollection.kt @@ -18,9 +18,13 @@ package opensavvy.ktmongo.sync import opensavvy.ktmongo.bson.BsonContext import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.expr.FilterExpression import opensavvy.ktmongo.dsl.expr.FilterOperators import opensavvy.ktmongo.dsl.expr.UpdateOperators import opensavvy.ktmongo.dsl.expr.UpsertOperators +import opensavvy.ktmongo.dsl.expr.common.toBsonDocument +import opensavvy.ktmongo.dsl.models.BulkWrite +import opensavvy.ktmongo.dsl.options.BulkWriteOptions import opensavvy.ktmongo.dsl.options.CountOptions import opensavvy.ktmongo.dsl.options.FindOptions import opensavvy.ktmongo.dsl.options.UpdateOptions @@ -123,6 +127,19 @@ private class FilteredCollection( update = update, ) + override fun bulkWrite( + options: BulkWriteOptions.() -> Unit, + filter: FilterOperators.() -> Unit, + operations: BulkWrite.() -> Unit + ) = upstream.bulkWrite( + options = options, + filter = { + globalFilter() + filter() + }, + operations = operations, + ) + @OptIn(LowLevelApi::class) override fun toString(): String { val filter = FilterExpression(context) diff --git a/driver-sync/src/commonMain/kotlin/MongoCollection.kt b/driver-sync/src/commonMain/kotlin/MongoCollection.kt index 27db3c63..e314eeab 100644 --- a/driver-sync/src/commonMain/kotlin/MongoCollection.kt +++ b/driver-sync/src/commonMain/kotlin/MongoCollection.kt @@ -25,6 +25,7 @@ import opensavvy.ktmongo.sync.operations.UpdateOperations * * ### Operations * + * - [bulkWrite][UpdateOperations.bulkWrite] * - [count][CountOperations.count] * - [countEstimated][CountOperations.countEstimated] * - [find][FindOperations.find] diff --git a/driver-sync/src/commonMain/kotlin/operations/UpdateOperations.kt b/driver-sync/src/commonMain/kotlin/operations/UpdateOperations.kt index 8390bb08..c5396c37 100644 --- a/driver-sync/src/commonMain/kotlin/operations/UpdateOperations.kt +++ b/driver-sync/src/commonMain/kotlin/operations/UpdateOperations.kt @@ -19,6 +19,8 @@ package opensavvy.ktmongo.sync.operations import opensavvy.ktmongo.dsl.expr.FilterOperators import opensavvy.ktmongo.dsl.expr.UpdateOperators import opensavvy.ktmongo.dsl.expr.UpsertOperators +import opensavvy.ktmongo.dsl.models.BulkWrite +import opensavvy.ktmongo.dsl.options.BulkWriteOptions import opensavvy.ktmongo.dsl.options.UpdateOptions import opensavvy.ktmongo.sync.MongoCollection import opensavvy.ktmongo.sync.filter @@ -230,4 +232,74 @@ interface UpdateOperations : BaseOperations { update: UpdateOperators.() -> Unit, ): Document? + /** + * Performs multiple update operations in a single request. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * upsertOne( + * filter = { + * User::name eq "Patrick" + * }, + * update = { + * User::age set 15 + * } + * ) + * + * updateMany { + * User::age inc 1 + * } + * } + * ``` + * + * To see which operations are available and their respective syntax, see [BulkWrite]. + * + * ### Using filtered writes + * + * We can group operations by the filter they apply on: + * ```kotlin + * collection.bulkWrite { + * filtered(filter = { User::isAlive eq true }) { + * updateOne(…) + * updateOne(…) + * updateMany(…) + * } + * + * updateOne(…) + * } + * ``` + * + * To learn more, see [filtered][BulkWrite.filtered]. + * + * ### Using filtered collections + * + * If we want all operations to use the same filter, we can declare it before calling + * the operation: + * ```kotlin + * collection.filter { + * User::isAlive eq true + * }.bulkWrite { + * updateOne(…) + * updateOne(…) + * updateMany(…) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite) + */ + fun bulkWrite( + options: BulkWriteOptions.() -> Unit = {}, + filter: FilterOperators.() -> Unit = {}, + operations: BulkWrite.() -> Unit, + ) + } diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt index 361debe9..2170e3dc 100644 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -23,6 +23,7 @@ import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.expr.* import opensavvy.ktmongo.dsl.expr.common.toBsonDocument import opensavvy.ktmongo.dsl.models.* +import opensavvy.ktmongo.dsl.options.BulkWriteOptions import opensavvy.ktmongo.dsl.options.CountOptions import opensavvy.ktmongo.dsl.options.FindOptions import opensavvy.ktmongo.dsl.options.common.LimitOption @@ -156,6 +157,23 @@ class JvmMongoCollection internal constructor( return inner.findOneAndUpdate(model.filter.toBsonDocument(), model.update.toBsonDocument(), FindOneAndUpdateOptions()) } + @OptIn(LowLevelApi::class) + override fun bulkWrite( + options: BulkWriteOptions.() -> Unit, + filter: FilterOperators.() -> Unit, + operations: BulkWrite.() -> Unit, + ) { + val model = BulkWrite(context, filter) + + model.options.options() + model.operations() + + inner.bulkWrite( + model.operations.map { it.toJava() }.toList(), + options = com.mongodb.client.model.BulkWriteOptions() + ) + } + // endregion override fun toString(): String = -- 2.51.2 From 56474d76327ff5611f6e50e3ddb7545548d1c535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 12 Nov 2024 22:36:51 +0100 Subject: [PATCH 8/8] feat(driver-coroutines): Introduce bulkWrite --- .../commonMain/kotlin/FilteredCollection.kt | 15 ++++ .../src/commonMain/kotlin/MongoCollection.kt | 1 + .../kotlin/operations/UpdateOperations.kt | 73 +++++++++++++++++++ .../src/jvmMain/kotlin/JvmMongoCollection.kt | 18 +++++ 4 files changed, 107 insertions(+) diff --git a/driver-coroutines/src/commonMain/kotlin/FilteredCollection.kt b/driver-coroutines/src/commonMain/kotlin/FilteredCollection.kt index 5cfa21ae..29fc2dfe 100644 --- a/driver-coroutines/src/commonMain/kotlin/FilteredCollection.kt +++ b/driver-coroutines/src/commonMain/kotlin/FilteredCollection.kt @@ -23,6 +23,8 @@ import opensavvy.ktmongo.dsl.expr.FilterOperators import opensavvy.ktmongo.dsl.expr.UpdateOperators import opensavvy.ktmongo.dsl.expr.UpsertOperators import opensavvy.ktmongo.dsl.expr.common.toBsonDocument +import opensavvy.ktmongo.dsl.models.BulkWrite +import opensavvy.ktmongo.dsl.options.BulkWriteOptions import opensavvy.ktmongo.dsl.options.CountOptions import opensavvy.ktmongo.dsl.options.FindOptions import opensavvy.ktmongo.dsl.options.UpdateOptions @@ -125,6 +127,19 @@ private class FilteredCollection( update = update, ) + override suspend fun bulkWrite( + options: BulkWriteOptions.() -> Unit, + filter: FilterOperators.() -> Unit, + operations: BulkWrite.() -> Unit + ) = upstream.bulkWrite( + options = options, + filter = { + globalFilter() + filter() + }, + operations = operations, + ) + @OptIn(LowLevelApi::class) override fun toString(): String { val filter = FilterExpression(context) diff --git a/driver-coroutines/src/commonMain/kotlin/MongoCollection.kt b/driver-coroutines/src/commonMain/kotlin/MongoCollection.kt index d55faecb..441a4cf3 100644 --- a/driver-coroutines/src/commonMain/kotlin/MongoCollection.kt +++ b/driver-coroutines/src/commonMain/kotlin/MongoCollection.kt @@ -25,6 +25,7 @@ import opensavvy.ktmongo.coroutines.operations.UpdateOperations * * ### Operations * + * - [bulkWrite][UpdateOperations.bulkWrite] * - [count][CountOperations.count] * - [countEstimated][CountOperations.countEstimated] * - [find][FindOperations.find] diff --git a/driver-coroutines/src/commonMain/kotlin/operations/UpdateOperations.kt b/driver-coroutines/src/commonMain/kotlin/operations/UpdateOperations.kt index 1de90cee..ff1488e9 100644 --- a/driver-coroutines/src/commonMain/kotlin/operations/UpdateOperations.kt +++ b/driver-coroutines/src/commonMain/kotlin/operations/UpdateOperations.kt @@ -21,6 +21,8 @@ import opensavvy.ktmongo.coroutines.filter import opensavvy.ktmongo.dsl.expr.FilterOperators import opensavvy.ktmongo.dsl.expr.UpdateOperators import opensavvy.ktmongo.dsl.expr.UpsertOperators +import opensavvy.ktmongo.dsl.models.BulkWrite +import opensavvy.ktmongo.dsl.options.BulkWriteOptions import opensavvy.ktmongo.dsl.options.UpdateOptions /** @@ -230,4 +232,75 @@ interface UpdateOperations : BaseOperations { update: UpdateOperators.() -> Unit, ): Document? + /** + * Performs multiple update operations in a single request. + * + * ### Example + * + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * collection.bulkWrite { + * upsertOne( + * filter = { + * User::name eq "Patrick" + * }, + * update = { + * User::age set 15 + * } + * ) + * + * updateMany { + * User::age inc 1 + * } + * } + * ``` + * + * To see which operations are available and their respective syntax, see [BulkWrite]. + * + * ### Using filtered writes + * + * We can group operations by the filter they apply on: + * ```kotlin + * collection.bulkWrite { + * filtered(filter = { User::isAlive eq true }) { + * updateOne(…) + * updateOne(…) + * updateMany(…) + * } + * + * updateOne(…) + * } + * ``` + * + * To learn more, see [filtered][BulkWrite.filtered]. + * + * ### Using filtered collections + * + * If we want all operations to use the same filter, we can declare it before calling + * the operation: + * ```kotlin + * collection.filter { + * User::isAlive eq true + * }.bulkWrite { + * updateOne(…) + * updateOne(…) + * updateMany(…) + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.bulkWrite) + */ + suspend fun bulkWrite( + options: BulkWriteOptions.() -> Unit = {}, + filter: FilterOperators.() -> Unit = {}, + operations: BulkWrite.() -> Unit, + ) + + } diff --git a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt index b2d12354..738108e0 100644 --- a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -23,6 +23,7 @@ import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.expr.* import opensavvy.ktmongo.dsl.expr.common.toBsonDocument import opensavvy.ktmongo.dsl.models.* +import opensavvy.ktmongo.dsl.options.BulkWriteOptions import opensavvy.ktmongo.dsl.options.CountOptions import opensavvy.ktmongo.dsl.options.FindOptions import opensavvy.ktmongo.dsl.options.common.LimitOption @@ -156,6 +157,23 @@ class JvmMongoCollection internal constructor( return inner.findOneAndUpdate(model.filter.toBsonDocument(), model.update.toBsonDocument(), FindOneAndUpdateOptions()) } + @OptIn(LowLevelApi::class) + override suspend fun bulkWrite( + options: BulkWriteOptions.() -> Unit, + filter: FilterOperators.() -> Unit, + operations: BulkWrite.() -> Unit, + ) { + val model = BulkWrite(context, filter) + + model.options.options() + model.operations() + + inner.bulkWrite( + model.operations.map { it.toJava() }.toList(), + options = com.mongodb.client.model.BulkWriteOptions() + ) + } + // endregion override fun toString(): String =