diff --git a/dsl/src/commonMain/kotlin/options/WriteConcernOption.kt b/dsl/src/commonMain/kotlin/options/WriteConcernOption.kt new file mode 100644 index 00000000..a8e11634 --- /dev/null +++ b/dsl/src/commonMain/kotlin/options/WriteConcernOption.kt @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2025, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.dsl.options + +import opensavvy.ktmongo.bson.BsonContext +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.KtMongoDsl +import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.time.Duration + +/** + * Specifies the write concern for an operation. + * + * To learn more about read concerns, see [WriteConcern]. + * To apply this option, see [WithWriteConcern]. + */ +class WriteConcernOption( + val concern: WriteConcern, + context: BsonContext, +) : AbstractOption("writeConcern", context) { + + @LowLevelApi + override fun write(writer: BsonValueWriter) = with(writer) { + writeDocument { + if (concern.acknowledgment != null) { + write("w") { + when (concern.acknowledgment) { + WriteAcknowledgment.Majority -> writeString("majority") + is WriteAcknowledgment.Nodes -> writeInt32(concern.acknowledgment.count) + is WriteAcknowledgment.Tagged -> writeString(concern.acknowledgment.tag) + } + } + } + + if (concern.writeToJournal != null) { + writeBoolean("j", concern.writeToJournal) + } + + if (concern.writeTimeout != null) { + writeInt64("wtimeout", concern.writeTimeout.inWholeMilliseconds) + } + } + } +} + +/** + * The level of acknowledgment requested from a write operation. + * + * See [WithWriteConcern.writeConcern]. + */ +class WriteConcern( + + /** + * Describes how many nodes must acknowledge the write operation. + * + * For the different options, see: + * - [WriteAcknowledgment.Majority]: majority of the nodes. + * - [WriteAcknowledgment.Nodes]: a given number of nodes. + * - [WriteAcknowledgment.Tagged]: specific nodes selected ahead of time. + * + * If this field is `null` (default), the MongoDB default acknowledgment applies, which is + * [WriteAcknowledgment.Majority] in most deployments. + * + * ### Example + * + * ```kotlin + * collections.updateMany( + * options = { + * writeConcern(WriteConcern(WriteAcknowledgment.Nodes(2)) + * } + * ) { + * User::age inc 1 + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/write-concern/#w-option) + * + * @see WithWriteConcern.writeConcern Specify this option. + */ + val acknowledgment: WriteAcknowledgment? = null, + + /** + * Specifies whether the write operation must acknowledge being written to the journal. + * + * Once an operation has been written to the journal, it is much less likely that it will be rolled back, + * even if in case of failure. + * However, roll back may still happen. + * To learn more, see [the journal documentation](https://www.mongodb.com/docs/manual/core/journaling/#std-label-journaling-internals). + * + * If set to `true`, the nodes specified by [acknowledgment] must additionally write the operation to their journal, + * which forces the journal to be written to disk. + * + * If this field is `null` (default), the MongoDB default applies, which depends on the deployment configuration. + * For example, on some deployments, [WriteAcknowledgment.Majority] implies that [writeToJournal] is set to `true`. + * To avoid surprises, we recommend always specifying this option. + * + * ### Example + * + * ```kotlin + * collections.updateMany( + * options = { + * writeConcern(WriteConcern(writeToJournal = true)) + * } + * ) { + * User::age inc 1 + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option) + * + * @see WithWriteConcern.writeConcern Specify this option. + */ + val writeToJournal: Boolean? = null, + + /** + * Specifies a time limit for a write operation to propagate to enough members to achieve [acknowledgment] and [writeToJournal]. + * + * If [acknowledgment] is set to 0 or 1 nodes, this setting does nothing. + * + * If the write operation cannot be acknowledged in less than this timeout, MongoDB returns a write concern exception, + * even if the operation may have later succeeded. + * In that case, the operations **are not undone**. + * + * You may also be interested in the [`maxTime`][WithMaxTime.maxTime] option. + * + * If this option is set to `null` and the [acknowledgment] and [writeToJournal] options are unachievable, the write operation + * will block indefinitely. + * + * [writeTimeout] of 0 is equivalent to `null`. + * + * ### Example + * + * ```kotlin + * collections.updateMany( + * options = { + * writeConcern(WriteConcern(writeTimeout = 2.minutes)) + * } + * ) { + * User::age inc 1 + * } + * ``` + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout) + * + * @see WithWriteConcern.writeConcern Specify this option. + */ + val writeTimeout: Duration? = null, +) { + + fun copy( + acknowledgment: WriteAcknowledgment? = null, + writeToJournal: Boolean? = null, + writeTimeout: Duration? = null, + ): WriteConcern = + WriteConcern(acknowledgment ?: this.acknowledgment, writeToJournal ?: this.writeToJournal, writeTimeout + ?: this.writeTimeout) + + companion object { + + /** + * Requests acknowledgement from the majority of data-bearing members, as well as requesting that the write is written to the journal. + * + * Use this write concern for requests where the result is important and shouldn't be lost. + * Note that some data may still be rolled back, see [writeToJournal]. + * + * ### Example + * + * ```kotlin + * collections.updateMany( + * options = { + * writeConcern(WriteConcern.Majority) + * } + * ) { + * User::age inc 1 + * } + * ``` + * + * @see WithWriteConcern.writeConcern Specify this option. + */ + val Majority = WriteConcern(WriteAcknowledgment.Majority, writeToJournal = true) + + /** + * Requests acknowledgement from the primary node only. + * + * This ensures that the primary node has successfully handled this write operation. + * If the primary changes or dies before this write has been replicated, it may be rolled back. + * However, in most situations, this should be enough to ensure the data is stored. + * + * Use this write concern for requests where the result should preferably not be lost, but rare rollbacks + * are acceptable in exchange for lower latency. + * + * ### Example + * + * ```kotlin + * collections.updateMany( + * options = { + * writeConcern(WriteConcern.Primary) + * } + * ) { + * User::age inc 1 + * } + * ``` + * + * @see WithWriteConcern.writeConcern Specify this option. + */ + val Primary = WriteConcern(WriteAcknowledgment.Nodes(1), writeToJournal = false) + + /** + * Requests are not acknowledged. + * + * The MongoDB server will acknowledge having received the request, but will not communicate further, no matter what happens. + * + * Use this write concern for requests where the result's loss is acceptable. + * For example, when storing high-volume sensor data, it may be acceptable if some measurements are lost when the cluster + * is overloaded. + * + * ### Example + * + * ```kotlin + * collections.updateMany( + * options = { + * writeConcern(WriteConcern.FireAndForget) + * } + * ) { + * User::age inc 1 + * } + * ``` + * + * @see WithWriteConcern.writeConcern Specify this option. + */ + val FireAndForget = WriteConcern(WriteAcknowledgment.Nodes(0), writeToJournal = false) + } +} + +/** + * Describes how many nodes must acknowledge this write operation. + * + * For more information, see [WriteConcern]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/write-concern/#w-option) + */ +sealed class WriteAcknowledgment { + + /** + * Requests acknowledgment that the [calculated majority](https://www.mongodb.com/docs/manual/reference/write-concern/#std-label-calculating-majority-count) + * of data-bearing voting members have durably written the change to their local [oplog](https://www.mongodb.com/docs/manual/reference/glossary/#std-term-oplog). + * The members then asynchronously apply changes as they read them from their local oplogs. + * + * This value is the default for most deployments. + * + * After a write operation has received a majority acknowledgment, clients can read the results of that write with a + * [ReadConcern.Majority] read concern. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/write-concern/#mongodb-writeconcern-writeconcern.-majority-) + * + * @see WithWriteConcern.writeConcern Specify this option. + */ + data object Majority : WriteAcknowledgment() + + /** + * Requests acknowledgment that the write operation has propagated to the specified number of instances. + * + * If set to `1`, the write operation must be acknowledged by the standalone mongod or by the primary in a replica set. + * The data may be rolled back if the primary steps down before the write has replicated to secondaries. + * + * If set to `0`, no acknowledgment about the write itself are requested. + * However, the client may still receive transport exceptions (e.g. networking errors). + * The data may be rolled back if the primary steps down before the write has replicated to secondaries. + * + * If [WriteConcern.writeToJournal] is `true`, the request will acknowledge that it has been written to the journal, + * meaning that at least one node will acknowledge the write, even if this option is set to `0`. + * + * If set to a number greater than `1`, the primary must acknowledge the request, as well as as many data-bearing + * secondaries as needed to meet the specified write concern. That is, `2` requests acknowledgment from the primary and one secondary. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/write-concern/#mongodb-writeconcern-writeconcern.-number-) + * - [More about acknowledgment](https://www.mongodb.com/docs/manual/reference/write-concern/#std-label-wc-ack-behavior) + * + * @see WithWriteConcern.writeConcern Specify this option. + */ + data class Nodes( + /** + * See [Nodes]. + */ + val count: Int, + ) : WriteAcknowledgment() + + /** + * Requests acknowledgment from members [tagged](https://www.mongodb.com/docs/manual/reference/replica-configuration/#mongodb-rsconf-rsconf.members-n-.tags) with [tag]. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/write-concern/#mongodb-writeconcern-writeconcern.-custom-write-concern-name-) + * - [Official example](https://www.mongodb.com/docs/manual/tutorial/configure-replica-set-tag-sets/#std-label-configure-custom-write-concern) + * + * @see WithWriteConcern.writeConcern Specify this option. + */ + data class Tagged( + val tag: String, + ) : WriteAcknowledgment() +} + +/** + * Consistency guarantees for this request. + * + * See [writeConcern]. + */ +interface WithWriteConcern : Options { + + /** + * Specifies the [WriteConcern] for this operation. + * + * The write concern specifies which nodes must acknowledge having applied this write operation. + * The stronger the write concern, the less chance of data loss, but the higher the latency. + * + * To learn more about the different options, see: + * - [WriteConcern.acknowledgment]: how many nodes should acknowledge this request? + * - [WriteConcern.writeToJournal]: should the nodes also acknowledge synchronizing their journal? + * - [WriteConcern.writeTimeout]: after how long should we give up on the acknowledgment, if it doesn't succeed? + * + * ### Example + * + * ```kotlin + * collections.updateMany( + * options = { + * writeConcern(WriteConcern(Majority, writeTimeout = 2.seconds)) + * } + * ) { + * User::age inc 1 + * } + * ``` + * + * ### Convenience helpers + * + * For convenience of the most common scenarii, KtMongo provides the following helpers: + * - [WriteConcern.Majority]: requests strong acknowledgment from the majority of nodes. + * - [WriteConcern.Primary]: requests weak acknowledgement from the primary node. + * - [WriteConcern.FireAndForget]: requests no acknowledgement at all. + * + * ### Transactions + * + * In multi-document transactions, only specify a write concern at the transaction level, and not at the level + * of individual operation. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/write-concern) + */ + @KtMongoDsl + @OptIn(DangerousMongoApi::class, LowLevelApi::class) + fun writeConcern(concern: WriteConcern) { + accept(WriteConcernOption(concern, context)) + } + +} diff --git a/dsl/src/commonTest/kotlin/options/WriteConcernTest.kt b/dsl/src/commonTest/kotlin/options/WriteConcernTest.kt new file mode 100644 index 00000000..98db1ae9 --- /dev/null +++ b/dsl/src/commonTest/kotlin/options/WriteConcernTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2025, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.dsl.options + +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.command.UpdateOptions +import opensavvy.ktmongo.dsl.query.shouldBeBson +import opensavvy.ktmongo.dsl.query.testContext +import opensavvy.prepared.runner.kotest.PreparedSpec +import kotlin.time.Duration.Companion.minutes + +@LowLevelApi +class WriteConcernTest : PreparedSpec({ + + test("Set majority acknowledgment") { + val options = UpdateOptions(testContext()) + + options.writeConcern(WriteConcern(WriteAcknowledgment.Majority)) + + options.toString() shouldBeBson """ + { + "writeConcern": { + "w": "majority" + } + } + """.trimIndent() + } + + test("Set acknowledgement of 5 nodes") { + val options = UpdateOptions(testContext()) + + options.writeConcern(WriteConcern(WriteAcknowledgment.Nodes(5))) + + options.toString() shouldBeBson """ + { + "writeConcern": { + "w": 5 + } + } + """.trimIndent() + } + + test("Set acknowledgement with a tag") { + val options = UpdateOptions(testContext()) + + options.writeConcern(WriteConcern(WriteAcknowledgment.Tagged("backup"))) + + options.toString() shouldBeBson """ + { + "writeConcern": { + "w": "backup" + } + } + """.trimIndent() + } + + test("Set acknowledgement to the journal") { + val options = UpdateOptions(testContext()) + + options.writeConcern(WriteConcern(writeToJournal = true)) + + options.toString() shouldBeBson """ + { + "writeConcern": { + "j": true + } + } + """.trimIndent() + } + + test("Set an acknowledgment timeout") { + val options = UpdateOptions(testContext()) + + options.writeConcern(WriteConcern(writeTimeout = 2.minutes)) + + options.toString() shouldBeBson """ + { + "writeConcern": { + "wtimeout": 120000 + } + } + """.trimIndent() + } + + test("Everything together") { + val options = UpdateOptions(testContext()) + + options.writeConcern( + WriteConcern( + acknowledgment = WriteAcknowledgment.Majority, + writeToJournal = false, + writeTimeout = 2.minutes, + ) + ) + + options.toString() shouldBeBson """ + { + "writeConcern": { + "w": "majority", + "j": false, + "wtimeout": 120000 + } + } + """.trimIndent() + } + + test("Shortcut: majority") { + val options = UpdateOptions(testContext()) + + options.writeConcern(WriteConcern.Majority) + + options.toString() shouldBeBson """ + { + "writeConcern": { + "w": "majority", + "j": true + } + } + """.trimIndent() + } + + test("Shortcut: primary") { + val options = UpdateOptions(testContext()) + + options.writeConcern(WriteConcern.Primary) + + options.toString() shouldBeBson """ + { + "writeConcern": { + "w": 1, + "j": false + } + } + """.trimIndent() + } + + test("Shortcut: primary") { + val options = UpdateOptions(testContext()) + + options.writeConcern(WriteConcern.FireAndForget) + + options.toString() shouldBeBson """ + { + "writeConcern": { + "w": 0, + "j": false + } + } + """.trimIndent() + } + +}) -- 2.51.2 From f3047d379706f2583d7762aebad43694079e5d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 8 Jul 2025 22:38:02 +0200 Subject: [PATCH 2/3] feat(dsl): Added the writeConcern option to all commands --- .../src/jvmMain/kotlin/JvmMongoCollection.kt | 53 +++++++++++------ .../src/jvmMain/kotlin/BsonUtils.kt | 26 ++++++++ .../src/jvmMain/kotlin/JvmMongoCollection.kt | 59 ++++++++++++------- .../commonMain/kotlin/command/BulkWrite.kt | 4 +- dsl/src/commonMain/kotlin/command/Delete.kt | 7 ++- dsl/src/commonMain/kotlin/command/Drop.kt | 4 +- dsl/src/commonMain/kotlin/command/Insert.kt | 7 ++- dsl/src/commonMain/kotlin/command/Update.kt | 4 +- 8 files changed, 119 insertions(+), 45 deletions(-) diff --git a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt index 3097ad62..29798361 100644 --- a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -26,12 +26,17 @@ import opensavvy.ktmongo.bson.official.JvmBsonContext import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.aggregation.PipelineChainLink import opensavvy.ktmongo.dsl.command.* +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.options.WriteConcernOption +import opensavvy.ktmongo.dsl.options.option import opensavvy.ktmongo.dsl.query.FilterQuery import opensavvy.ktmongo.dsl.query.UpdateQuery import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery import opensavvy.ktmongo.dsl.query.UpsertQuery import opensavvy.ktmongo.official.command.toJava import opensavvy.ktmongo.official.options.* +import opensavvy.ktmongo.official.options.toJava +import opensavvy.ktmongo.official.toJava import java.util.concurrent.TimeUnit /** @@ -118,7 +123,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateMany(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions()) + inner.withWriteConcern(model.options).updateMany(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions()) } @OptIn(LowLevelApi::class) @@ -133,7 +138,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateOne(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions()) + inner.withWriteConcern(model.options).updateOne(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions()) } @OptIn(LowLevelApi::class) @@ -148,7 +153,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateOne(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions().upsert(true)) + inner.withWriteConcern(model.options).updateOne(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions().upsert(true)) } @OptIn(LowLevelApi::class) @@ -163,7 +168,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - return inner.findOneAndUpdate(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, FindOneAndUpdateOptions()) + return inner.withWriteConcern(model.options).findOneAndUpdate(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, FindOneAndUpdateOptions()) } @OptIn(LowLevelApi::class) @@ -177,7 +182,7 @@ class JvmMongoCollection internal constructor( model.options.options() model.operations() - inner.bulkWrite( + inner.withWriteConcern(model.options).bulkWrite( model.operations.map { it.toJava() }.toList(), options = com.mongodb.client.model.BulkWriteOptions() ) @@ -198,7 +203,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateMany(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) + inner.withWriteConcern(model.options).updateMany(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) } @OptIn(LowLevelApi::class) @@ -213,7 +218,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateOne(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) + inner.withWriteConcern(model.options).updateOne(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) } @OptIn(LowLevelApi::class) @@ -228,7 +233,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateOne(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions().upsert(true)) + inner.withWriteConcern(model.options).updateOne(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions().upsert(true)) } // endregion @@ -240,7 +245,7 @@ class JvmMongoCollection internal constructor( model.options.options() - inner.insertOne( + inner.withWriteConcern(model.options).insertOne( model.document, com.mongodb.client.model.InsertOneOptions() ) @@ -252,7 +257,7 @@ class JvmMongoCollection internal constructor( model.options.options() - inner.insertMany( + inner.withWriteConcern(model.options).insertMany( model.documents, com.mongodb.client.model.InsertManyOptions() ) @@ -266,11 +271,13 @@ class JvmMongoCollection internal constructor( options: DeleteOneOptions.() -> Unit, filter: FilterQuery.() -> Unit, ) { - DeleteOneOptions(context).apply(options) - val filter = FilterQuery(context).apply(filter) + val model = DeleteOne(context) - inner.deleteOne( - filter = context.buildDocument(filter).raw, + model.filter.filter() + model.options.options() + + inner.withWriteConcern(model.options).deleteOne( + filter = context.buildDocument(model.filter).raw, options = DeleteOptions() ) } @@ -280,11 +287,13 @@ class JvmMongoCollection internal constructor( options: DeleteManyOptions.() -> Unit, filter: FilterQuery.() -> Unit, ) { - DeleteManyOptions(context).apply(options) - val filter = FilterQuery(context).apply(filter) + val model = DeleteMany(context) - inner.deleteOne( - filter = context.buildDocument(filter).raw, + model.filter.filter() + model.options.options() + + inner.withWriteConcern(model.options).deleteOne( + filter = context.buildDocument(model.filter).raw, options = DeleteOptions() ) } @@ -338,3 +347,11 @@ class JvmMongoCollection internal constructor( */ fun com.mongodb.kotlin.client.coroutine.MongoCollection.asKtMongo(): JvmMongoCollection = JvmMongoCollection(this) + +@LowLevelApi +private fun com.mongodb.kotlin.client.coroutine.MongoCollection.withWriteConcern(option: WithWriteConcern): com.mongodb.kotlin.client.coroutine.MongoCollection { + val concern = option.option()?.concern + ?: return this + + return this.withWriteConcern(concern.toJava()) +} diff --git a/driver-shared-official/src/jvmMain/kotlin/BsonUtils.kt b/driver-shared-official/src/jvmMain/kotlin/BsonUtils.kt index da6b22ee..86912bb4 100644 --- a/driver-shared-official/src/jvmMain/kotlin/BsonUtils.kt +++ b/driver-shared-official/src/jvmMain/kotlin/BsonUtils.kt @@ -18,8 +18,11 @@ package opensavvy.ktmongo.official import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.options.ReadConcern +import opensavvy.ktmongo.dsl.options.WriteAcknowledgment +import opensavvy.ktmongo.dsl.options.WriteConcern import opensavvy.ktmongo.dsl.tree.BsonNode import org.bson.conversions.Bson +import java.util.concurrent.TimeUnit @LowLevelApi fun opensavvy.ktmongo.bson.Bson.toJava(): Bson = (this as opensavvy.ktmongo.bson.official.Bson).raw @@ -36,3 +39,26 @@ fun ReadConcern?.toJava(): com.mongodb.ReadConcern = when (this) { ReadConcern.Snapshot -> com.mongodb.ReadConcern.SNAPSHOT null -> com.mongodb.ReadConcern.DEFAULT } + +@LowLevelApi +fun WriteConcern.toJava(): com.mongodb.WriteConcern { + var ret = com.mongodb.WriteConcern.ACKNOWLEDGED + + ret = when (val ack = acknowledgment) { + WriteAcknowledgment.Majority -> ret.withW("majority") + is WriteAcknowledgment.Nodes -> ret.withW(ack.count) + is WriteAcknowledgment.Tagged -> ret.withW(ack.tag) + null -> ret // Nothing to do + } + + if (writeToJournal != null) { + ret = ret.withJournal(writeToJournal) + } + + val timeout = writeTimeout + if (timeout != null) { + ret = ret.withWTimeout(timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS) + } + + return ret +} diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt index 5e4ceab8..3c486b2b 100644 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -24,12 +24,17 @@ import opensavvy.ktmongo.bson.official.JvmBsonContext import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.aggregation.PipelineChainLink import opensavvy.ktmongo.dsl.command.* +import opensavvy.ktmongo.dsl.options.WithWriteConcern +import opensavvy.ktmongo.dsl.options.WriteConcernOption +import opensavvy.ktmongo.dsl.options.option import opensavvy.ktmongo.dsl.query.FilterQuery import opensavvy.ktmongo.dsl.query.UpdateQuery import opensavvy.ktmongo.dsl.query.UpdateWithPipelineQuery import opensavvy.ktmongo.dsl.query.UpsertQuery import opensavvy.ktmongo.official.command.toJava import opensavvy.ktmongo.official.options.* +import opensavvy.ktmongo.official.options.toJava +import opensavvy.ktmongo.official.toJava import java.util.concurrent.TimeUnit /** @@ -116,7 +121,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateMany(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions()) + inner.withWriteConcern(model.options).updateMany(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions()) } @OptIn(LowLevelApi::class) @@ -131,7 +136,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateOne(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions()) + inner.withWriteConcern(model.options).updateOne(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions()) } @OptIn(LowLevelApi::class) @@ -146,7 +151,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateOne(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions().upsert(true)) + inner.withWriteConcern(model.options).updateOne(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, UpdateOptions().upsert(true)) } @OptIn(LowLevelApi::class) @@ -161,7 +166,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - return inner.findOneAndUpdate(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, FindOneAndUpdateOptions()) + return inner.withWriteConcern(model.options).findOneAndUpdate(context.buildDocument(model.filter).raw, context.buildDocument(model.update).raw, FindOneAndUpdateOptions()) } @OptIn(LowLevelApi::class) @@ -175,7 +180,7 @@ class JvmMongoCollection internal constructor( model.options.options() model.operations() - inner.bulkWrite( + inner.withWriteConcern(model.options).bulkWrite( model.operations.map { it.toJava() }.toList(), options = com.mongodb.client.model.BulkWriteOptions() ) @@ -196,7 +201,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateMany(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) + inner.withWriteConcern(model.options).updateMany(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) } @OptIn(LowLevelApi::class) @@ -211,7 +216,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateOne(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) + inner.withWriteConcern(model.options).updateOne(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions()) } @OptIn(LowLevelApi::class) @@ -226,7 +231,7 @@ class JvmMongoCollection internal constructor( model.filter.filter() model.update.update() - inner.updateOne(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions().upsert(true)) + inner.withWriteConcern(model.options).updateOne(context.buildDocument(model.filter).raw, model.updates.map { it.toJava() }, UpdateOptions().upsert(true)) } // endregion @@ -238,7 +243,7 @@ class JvmMongoCollection internal constructor( model.options.options() - inner.insertOne( + inner.withWriteConcern(model.options).insertOne( model.document, com.mongodb.client.model.InsertOneOptions() ) @@ -250,7 +255,7 @@ class JvmMongoCollection internal constructor( model.options.options() - inner.insertMany( + inner.withWriteConcern(model.options).insertMany( model.documents, com.mongodb.client.model.InsertManyOptions() ) @@ -264,11 +269,13 @@ class JvmMongoCollection internal constructor( options: DeleteOneOptions.() -> Unit, filter: FilterQuery.() -> Unit, ) { - DeleteOneOptions(context).apply(options) - val filter = FilterQuery(context).apply(filter) + val model = DeleteOne(context) - inner.deleteOne( - filter = context.buildDocument(filter).raw, + model.filter.filter() + model.options.options() + + inner.withWriteConcern(model.options).deleteOne( + filter = context.buildDocument(model.filter).raw, options = DeleteOptions() ) } @@ -278,11 +285,13 @@ class JvmMongoCollection internal constructor( options: DeleteManyOptions.() -> Unit, filter: FilterQuery.() -> Unit, ) { - DeleteManyOptions(context).apply(options) - val filter = FilterQuery(context).apply(filter) + val model = DeleteMany(context) + + model.filter.filter() + model.options.options() - inner.deleteOne( - filter = context.buildDocument(filter).raw, + inner.withWriteConcern(model.options).deleteOne( + filter = context.buildDocument(model.filter).raw, options = DeleteOptions() ) } @@ -292,9 +301,11 @@ class JvmMongoCollection internal constructor( @OptIn(LowLevelApi::class) override fun drop(options: DropOptions.() -> Unit) { - DropOptions(context).apply(options) + val model = Drop(context) - inner.drop(DropCollectionOptions()) + model.options.options() + + inner.withWriteConcern(model.options).drop(DropCollectionOptions()) } // endregion @@ -325,3 +336,11 @@ class JvmMongoCollection internal constructor( */ fun com.mongodb.kotlin.client.MongoCollection.asKtMongo(): JvmMongoCollection = JvmMongoCollection(this) + +@LowLevelApi +private fun com.mongodb.kotlin.client.MongoCollection.withWriteConcern(option: WithWriteConcern): com.mongodb.kotlin.client.MongoCollection { + val concern = option.option()?.concern + ?: return this + + return this.withWriteConcern(concern.toJava()) +} diff --git a/dsl/src/commonMain/kotlin/command/BulkWrite.kt b/dsl/src/commonMain/kotlin/command/BulkWrite.kt index beead65f..724a1756 100644 --- a/dsl/src/commonMain/kotlin/command/BulkWrite.kt +++ b/dsl/src/commonMain/kotlin/command/BulkWrite.kt @@ -22,6 +22,7 @@ import opensavvy.ktmongo.dsl.KtMongoDsl import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.options.Options import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern import opensavvy.ktmongo.dsl.query.FilterQuery import opensavvy.ktmongo.dsl.query.UpdateQuery import opensavvy.ktmongo.dsl.query.UpsertQuery @@ -380,4 +381,5 @@ class BulkWrite private constructor( * The options for a [BulkWrite] command. */ class BulkWriteOptions(context: BsonContext) : - Options by OptionsHolder(context) + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/Delete.kt b/dsl/src/commonMain/kotlin/command/Delete.kt index 54630127..8602ba14 100644 --- a/dsl/src/commonMain/kotlin/command/Delete.kt +++ b/dsl/src/commonMain/kotlin/command/Delete.kt @@ -21,6 +21,7 @@ import opensavvy.ktmongo.dsl.KtMongoDsl import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.options.Options import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern import opensavvy.ktmongo.dsl.query.FilterQuery import opensavvy.ktmongo.dsl.tree.ImmutableNode import opensavvy.ktmongo.dsl.tree.Node @@ -73,10 +74,12 @@ class DeleteMany private constructor( * The options for a [DeleteOne] command. */ class DeleteOneOptions(context: BsonContext) : - Options by OptionsHolder(context) + Options by OptionsHolder(context), + WithWriteConcern /** * The options for a [DeleteMany] command. */ class DeleteManyOptions(context: BsonContext) : - Options by OptionsHolder(context) + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/Drop.kt b/dsl/src/commonMain/kotlin/command/Drop.kt index 24624a42..4ccdf8f1 100644 --- a/dsl/src/commonMain/kotlin/command/Drop.kt +++ b/dsl/src/commonMain/kotlin/command/Drop.kt @@ -21,6 +21,7 @@ import opensavvy.ktmongo.dsl.KtMongoDsl import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.options.Options import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern /** * Deleting an entire collection at once. @@ -39,4 +40,5 @@ class Drop private constructor( */ @OptIn(LowLevelApi::class) class DropOptions(context: BsonContext) : - Options by OptionsHolder(context) + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/Insert.kt b/dsl/src/commonMain/kotlin/command/Insert.kt index 2e94e5e0..7bc03f1d 100644 --- a/dsl/src/commonMain/kotlin/command/Insert.kt +++ b/dsl/src/commonMain/kotlin/command/Insert.kt @@ -20,6 +20,7 @@ import opensavvy.ktmongo.bson.BsonContext import opensavvy.ktmongo.dsl.KtMongoDsl import opensavvy.ktmongo.dsl.options.Options import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern import opensavvy.ktmongo.dsl.tree.ImmutableNode import opensavvy.ktmongo.dsl.tree.Node @@ -71,10 +72,12 @@ class InsertMany private constructor( * The options for a `collection.insertOne` operation. */ class InsertOneOptions(context: BsonContext) : - Options by OptionsHolder(context) + Options by OptionsHolder(context), + WithWriteConcern /** * The options for a `collection.insertMany` operation. */ class InsertManyOptions(context: BsonContext) : - Options by OptionsHolder(context) + Options by OptionsHolder(context), + WithWriteConcern diff --git a/dsl/src/commonMain/kotlin/command/Update.kt b/dsl/src/commonMain/kotlin/command/Update.kt index 7b1ca2a1..d74a31ba 100644 --- a/dsl/src/commonMain/kotlin/command/Update.kt +++ b/dsl/src/commonMain/kotlin/command/Update.kt @@ -21,6 +21,7 @@ import opensavvy.ktmongo.dsl.KtMongoDsl import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.options.Options import opensavvy.ktmongo.dsl.options.OptionsHolder +import opensavvy.ktmongo.dsl.options.WithWriteConcern import opensavvy.ktmongo.dsl.query.FilterQuery import opensavvy.ktmongo.dsl.query.UpdateQuery import opensavvy.ktmongo.dsl.query.UpsertQuery @@ -109,4 +110,5 @@ class UpdateMany private constructor( * The options for a [UpdateOne], [UpsertOne], [UpdateMany] operation. */ class UpdateOptions(context: BsonContext) : - Options by OptionsHolder(context) + Options by OptionsHolder(context), + WithWriteConcern -- 2.51.2 From dcc6df4b59611c7199bd006eb4f3b072196e8a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 8 Jul 2025 22:38:50 +0200 Subject: [PATCH 3/3] style(driver-shared-official): Removed unused marker file --- .../src/jvmMain/kotlin/Marker.kt | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 driver-shared-official/src/jvmMain/kotlin/Marker.kt diff --git a/driver-shared-official/src/jvmMain/kotlin/Marker.kt b/driver-shared-official/src/jvmMain/kotlin/Marker.kt deleted file mode 100644 index ddfc2e9a..00000000 --- a/driver-shared-official/src/jvmMain/kotlin/Marker.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2025, OpenSavvy and contributors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package opensavvy.ktmongo.official