From f7e48e880b19c402b674ff027ea208aac37638de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 8 Sep 2026 19:14:17 +0200 Subject: [PATCH] feat(dsl): Introduce MongoWriteException and verify it is thrown when an insert fails --- .../kotlin/command/errors/Exceptions.kt | 75 ++++++++++++++++++ .../kotlin/command/errors/Exceptions.kt | 78 +++++++++++++++++++ .../operations/InsertOperations.test.kt | 26 +++++++ 3 files changed, 179 insertions(+) create mode 100644 dsl-template/src/commonMain/kotlin/command/errors/Exceptions.kt create mode 100644 dsl/src/commonMain/kotlin/command/errors/Exceptions.kt diff --git a/dsl-template/src/commonMain/kotlin/command/errors/Exceptions.kt b/dsl-template/src/commonMain/kotlin/command/errors/Exceptions.kt new file mode 100644 index 00000000..d4236475 --- /dev/null +++ b/dsl-template/src/commonMain/kotlin/command/errors/Exceptions.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, 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.command.errors + +import opensavvy.ktmongo.dsl.command.Command + +/** + * Something went wrong while exchanging information with the database. + */ +sealed class MongoException( + message: String, + cause: Throwable? = null, +) : RuntimeException(message, cause) { + + /** + * The address of any given server. + * + * This is mostly useful when a command fails to identify which member of a replica set the command was sent to. + */ + interface ServerAddress { + val host: String + val port: Int + } +} + +/** + * A write operation failed. + */ +class MongoWriteException( + message: String = "Write failed", + val server: ServerAddress, + val command: Command, + val errors: List, + val namespace: String, + cause: Throwable? = null, +) : MongoException( + message = buildString { + appendLine(message) + + for (error in errors) { + appendLine("\tfailed with ${error.message}") + } + + appendLine("\tat ${command::class.simpleName} $command") + appendLine("\tat $server $namespace") + }, + cause = cause, +) { + + /** + * Information about a failed write. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/insert/#mongodb-data-insert.writeErrors) + */ + interface WriteErrorData { + val code: Int + val message: String + } +} diff --git a/dsl/src/commonMain/kotlin/command/errors/Exceptions.kt b/dsl/src/commonMain/kotlin/command/errors/Exceptions.kt new file mode 100644 index 00000000..3fe8d000 --- /dev/null +++ b/dsl/src/commonMain/kotlin/command/errors/Exceptions.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026, 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. + */ + +// This file is generated from dsl-template/src/commonMain/kotlin/command/errors/Exceptions.kt +// DO NOT EDIT THIS FILE DIRECTLY. To learn more, read dsl-template/README.md. + +package opensavvy.ktmongo.dsl.command.errors + +import opensavvy.ktmongo.dsl.command.Command + +/** + * Something went wrong while exchanging information with the database. + */ +sealed class MongoException( + message: String, + cause: Throwable? = null, +) : RuntimeException(message, cause) { + + /** + * The address of any given server. + * + * This is mostly useful when a command fails to identify which member of a replica set the command was sent to. + */ + interface ServerAddress { + val host: String + val port: Int + } +} + +/** + * A write operation failed. + */ +class MongoWriteException( + message: String = "Write failed", + val server: ServerAddress, + val command: Command, + val errors: List, + val namespace: String, + cause: Throwable? = null, +) : MongoException( + message = buildString { + appendLine(message) + + for (error in errors) { + appendLine("\tfailed with ${error.message}") + } + + appendLine("\tat ${command::class.simpleName} $command") + appendLine("\tat $server $namespace") + }, + cause = cause, +) { + + /** + * Information about a failed write. + * + * ### External resources + * + * - [Official documentation](https://www.mongodb.com/docs/manual/reference/command/insert/#mongodb-data-insert.writeErrors) + */ + interface WriteErrorData { + val code: Int + val message: String + } +} diff --git a/test/src/commonMain/kotlin/operations/InsertOperations.test.kt b/test/src/commonMain/kotlin/operations/InsertOperations.test.kt index 6baf9b5b..4d0451fa 100644 --- a/test/src/commonMain/kotlin/operations/InsertOperations.test.kt +++ b/test/src/commonMain/kotlin/operations/InsertOperations.test.kt @@ -19,6 +19,8 @@ package opensavvy.ktmongo.tests.api.operations import kotlinx.serialization.Serializable import opensavvy.ktmongo.api.MongoClient import opensavvy.ktmongo.bson.types.ObjectId +import opensavvy.ktmongo.dsl.command.InsertOne +import opensavvy.ktmongo.dsl.command.errors.MongoWriteException import opensavvy.ktmongo.dsl.options.WriteConcern import opensavvy.ktmongo.tests.api.collection import opensavvy.prepared.suite.Prepared @@ -68,6 +70,30 @@ fun SuiteDsl.verifyInsertOperations( ) ) } + + test("Cannot insert two documents with the same ID") { + val id = collection().newId() + + val alice = InsertOperationsUser( + _id = id, + name = "Alice", + ) + + val bob = InsertOperationsUser( + _id = id, + name = "Bob", + ) + + collection().insertOne(alice) + + val e = checkThrows { + collection().insertOne(bob) + } + check((e.command as? InsertOne<*>)?.document == bob) + check(e.errors.size == 1) + check(e.errors[0].code == 11000) + check(e.namespace == collection().fullyQualifiedName) + } } suite("Options") { -- 2.51.2