diff --git a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt index 88472526..f3489db8 100644 --- a/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt +++ b/driver-coroutines/src/jvmMain/kotlin/CoroutineMongoCollectionImpl.kt @@ -154,6 +154,8 @@ private class CoroutineMongoCollectionImpl( ) } catch (e: com.mongodb.MongoWriteException) { throw e.toKtMongo(model, fullyQualifiedName) + } catch (e: com.mongodb.MongoCommandException) { + throw e.toKtMongo(model, fullyQualifiedName, factory) } } @@ -163,10 +165,14 @@ private class CoroutineMongoCollectionImpl( model.options.options() - inner.withWriteConcern(model.options).insertMany( - model.documents, - model.options.toJava(), - ) + try { + inner.withWriteConcern(model.options).insertMany( + model.documents, + model.options.toJava(), + ) + } catch (e: com.mongodb.MongoCommandException) { + throw e.toKtMongo(model, fullyQualifiedName, factory) + } } // endregion diff --git a/driver-multiplatform/src/commonMain/kotlin/ErrorHandling.kt b/driver-multiplatform/src/commonMain/kotlin/ErrorHandling.kt index 1a1a67b2..5afd3261 100644 --- a/driver-multiplatform/src/commonMain/kotlin/ErrorHandling.kt +++ b/driver-multiplatform/src/commonMain/kotlin/ErrorHandling.kt @@ -20,6 +20,7 @@ import opensavvy.ktmongo.bson.BsonType import opensavvy.ktmongo.bson.multiplatform.BsonDocument import opensavvy.ktmongo.dsl.command.Command import opensavvy.ktmongo.dsl.command.errors.MongoException +import opensavvy.ktmongo.dsl.command.errors.MongoSyntaxException import opensavvy.ktmongo.dsl.command.errors.MongoWriteException private class WriteErrorDataImpl( @@ -57,3 +58,24 @@ internal fun checkNoWriteErrors( errors = errors.map { WriteErrorDataImpl(it) }, ) } + +internal fun checkNoSyntaxErrors( + doc: BsonDocument, + command: Command, + collection: MultiplatformMongoCollection<*>, + server: MongoException.ServerAddress = collection.database.client.serverAddress, +) { + if (doc["ok"]?.decodeDouble() == 1.0) { + return // No errors, nothing to do + } + + throw MongoSyntaxException( + errorMessage = doc["errmsg"]?.decodeString() ?: "No error message were provided", + code = doc["code"]?.decodeInt32() ?: -1, + codeName = doc["codeName"]?.decodeString() ?: "", + fullResponse = doc, + command = command, + server = server, + namespace = collection.fullyQualifiedName, + ) +} diff --git a/driver-multiplatform/src/commonMain/kotlin/MultiplatformMongoCollectionImpl.kt b/driver-multiplatform/src/commonMain/kotlin/MultiplatformMongoCollectionImpl.kt index 0625aa29..dd8c4785 100644 --- a/driver-multiplatform/src/commonMain/kotlin/MultiplatformMongoCollectionImpl.kt +++ b/driver-multiplatform/src/commonMain/kotlin/MultiplatformMongoCollectionImpl.kt @@ -86,31 +86,33 @@ internal class MultiplatformMongoCollectionImpl( ) message as Message.OpMsg - check(message.body.document["ok"]?.decodeDouble() == 1.0) + checkNoSyntaxErrors(message.body.document, model, this) checkNoWriteErrors(message.body.document, model, this) } override suspend fun insertMany(documents: Iterable, options: InsertManyOptions.() -> Unit) { + val model = InsertMany( + context = database.client.context, + documents = documents.toList(), + documentType = type, + ).apply { + this.options.options() + } + val message = database.client.sendSingle( database.client.createDriverMessage { document { writeString("insert", name) writeString($$"$db", database.name) - InsertMany( - context = database.client.context, - documents = documents.toList(), - documentType = type, - ).apply { - this.options.options() - }.writeTo(this) + model.writeTo(this) } } ) message as Message.OpMsg - check(message.body.document["ok"]?.decodeDouble() == 1.0) { "Message is not OK: $message" } - check(message.body.document["writeErrors"] == null) { "Write errors occurred: $message" } + checkNoSyntaxErrors(message.body.document, model, this) + checkNoWriteErrors(message.body.document, model, this) } override fun filter(filter: FilterQuery.() -> Unit): MultiplatformMongoCollection = diff --git a/driver-shared-official/src/jvmMain/kotlin/Exceptions.jvm.kt b/driver-shared-official/src/jvmMain/kotlin/Exceptions.jvm.kt index dd9b4b90..e6937d07 100644 --- a/driver-shared-official/src/jvmMain/kotlin/Exceptions.jvm.kt +++ b/driver-shared-official/src/jvmMain/kotlin/Exceptions.jvm.kt @@ -16,10 +16,13 @@ package opensavvy.ktmongo.official +import opensavvy.ktmongo.bson.official.BsonFactory import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.command.Command import opensavvy.ktmongo.dsl.command.errors.MongoException +import opensavvy.ktmongo.dsl.command.errors.MongoSyntaxException import opensavvy.ktmongo.dsl.command.errors.MongoWriteException +import com.mongodb.MongoCommandException as OfficialMongoCommandException import com.mongodb.MongoWriteException as OfficialMongoWriteException import com.mongodb.ServerAddress as OfficialServerAddress @@ -60,3 +63,21 @@ fun OfficialMongoWriteException.toKtMongo( cause = this, ) } + +@LowLevelApi +fun OfficialMongoCommandException.toKtMongo( + command: Command, + namespace: String, + factory: BsonFactory, +): MongoSyntaxException { + return MongoSyntaxException( + errorMessage = errorMessage, + code = errorCode, + codeName = errorCodeName, + fullResponse = factory.readDocument(response), + server = serverAddress.toKtMongo(), + command = command, + namespace = namespace, + cause = this, + ) +} diff --git a/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt b/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt index 595ec8a0..905440ff 100644 --- a/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt +++ b/driver-sync/src/jvmMain/kotlin/SyncMongoCollectionImpl.kt @@ -154,6 +154,8 @@ private class SyncMongoCollectionImpl( ) } catch (e: com.mongodb.MongoWriteException) { throw e.toKtMongo(model, fullyQualifiedName) + } catch (e: com.mongodb.MongoCommandException) { + throw e.toKtMongo(model, fullyQualifiedName, factory) } } @@ -163,10 +165,14 @@ private class SyncMongoCollectionImpl( model.options.options() - inner.withWriteConcern(model.options).insertMany( - model.documents, - model.options.toJava(), - ) + try { + inner.withWriteConcern(model.options).insertMany( + model.documents, + model.options.toJava(), + ) + } catch (e: com.mongodb.MongoCommandException) { + throw e.toKtMongo(model, fullyQualifiedName, factory) + } } // endregion diff --git a/dsl-template/src/commonMain/kotlin/command/errors/Exceptions.kt b/dsl-template/src/commonMain/kotlin/command/errors/Exceptions.kt index d4236475..f846c28c 100644 --- a/dsl-template/src/commonMain/kotlin/command/errors/Exceptions.kt +++ b/dsl-template/src/commonMain/kotlin/command/errors/Exceptions.kt @@ -16,6 +16,7 @@ package opensavvy.ktmongo.dsl.command.errors +import opensavvy.ktmongo.bson.BsonDocument import opensavvy.ktmongo.dsl.command.Command /** @@ -37,6 +38,27 @@ sealed class MongoException( } } +/** + * MongoDB refused to execute a command because it is malformed. + */ +class MongoSyntaxException( + val errorMessage: String, + val code: Int, + val codeName: String, + val fullResponse: BsonDocument, + val server: ServerAddress, + val command: Command, + val namespace: String, + cause: Throwable? = null, +) : MongoException( + message = buildString { + appendLine("$code $codeName • $errorMessage") + appendLine("\tat ${command::class.simpleName} $command") + append("\tat $server $namespace") + }, + cause = cause, +) + /** * A write operation failed. */ @@ -56,7 +78,7 @@ class MongoWriteException( } appendLine("\tat ${command::class.simpleName} $command") - appendLine("\tat $server $namespace") + append("\tat $server $namespace") }, cause = cause, ) { diff --git a/dsl/src/commonMain/kotlin/command/errors/Exceptions.kt b/dsl/src/commonMain/kotlin/command/errors/Exceptions.kt index 3fe8d000..c60275b9 100644 --- a/dsl/src/commonMain/kotlin/command/errors/Exceptions.kt +++ b/dsl/src/commonMain/kotlin/command/errors/Exceptions.kt @@ -19,6 +19,7 @@ package opensavvy.ktmongo.dsl.command.errors +import opensavvy.ktmongo.bson.BsonDocument import opensavvy.ktmongo.dsl.command.Command /** @@ -40,6 +41,27 @@ sealed class MongoException( } } +/** + * MongoDB refused to execute a command because it is malformed. + */ +class MongoSyntaxException( + val errorMessage: String, + val code: Int, + val codeName: String, + val fullResponse: BsonDocument, + val server: ServerAddress, + val command: Command, + val namespace: String, + cause: Throwable? = null, +) : MongoException( + message = buildString { + appendLine("$code $codeName • $errorMessage") + appendLine("\tat ${command::class.simpleName} $command") + append("\tat $server $namespace") + }, + cause = cause, +) + /** * A write operation failed. */ @@ -59,7 +81,7 @@ class MongoWriteException( } appendLine("\tat ${command::class.simpleName} $command") - appendLine("\tat $server $namespace") + append("\tat $server $namespace") }, cause = cause, ) { diff --git a/test/src/commonMain/kotlin/operations/InsertOperations.test.kt b/test/src/commonMain/kotlin/operations/InsertOperations.test.kt index 931494f2..559d8124 100644 --- a/test/src/commonMain/kotlin/operations/InsertOperations.test.kt +++ b/test/src/commonMain/kotlin/operations/InsertOperations.test.kt @@ -25,8 +25,11 @@ import opensavvy.ktmongo.bson.BsonDocument import opensavvy.ktmongo.bson.decode import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.command.InsertMany import opensavvy.ktmongo.dsl.command.InsertOne +import opensavvy.ktmongo.dsl.command.errors.MongoSyntaxException import opensavvy.ktmongo.dsl.command.errors.MongoWriteException +import opensavvy.ktmongo.dsl.options.WriteAcknowledgment import opensavvy.ktmongo.dsl.options.WriteConcern import opensavvy.ktmongo.dsl.path.Field import opensavvy.ktmongo.tests.api.collection @@ -78,7 +81,7 @@ fun SuiteDsl.verifyInsertOperations( ) } - test("Cannot insert two documents with the same ID") { + test("insertOne • Cannot insert two documents with the same ID") { val id = collection().newId() val alice = InsertOperationsUser( @@ -101,6 +104,40 @@ fun SuiteDsl.verifyInsertOperations( check(e.errors[0].code == 11000) check(e.namespace == collection().fullyQualifiedName) } + + test("insertOne • Cannot set invalid options") { + val e = checkThrows { + collection().insertOne( + InsertOperationsUser( + _id = collection().newId(), + name = "Alice", + ), + options = { + // MongoDB doesn't accept a query with this many nodes + writeConcern(WriteAcknowledgment.Nodes(100)) + } + ) + } + check(e.command is InsertOne<*>) + check(e.codeName == "FailedToParse") + } + + test("insertMany • Cannot set invalid options") { + val e = checkThrows { + collection().insertMany( + InsertOperationsUser( + _id = collection().newId(), + name = "Alice", + ), + options = { + // MongoDB doesn't accept a query with this many nodes + writeConcern(WriteAcknowledgment.Nodes(100)) + } + ) + } + check(e.command is InsertMany<*>) + check(e.codeName == "FailedToParse") + } } test("Decreased type safety") {